diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index f579acb1..00000000 --- a/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -**/node_modules/** -**/Dockerfile diff --git a/.env.template b/.env.template index 0bd7bf6d..d0db42e3 100644 --- a/.env.template +++ b/.env.template @@ -5,27 +5,3 @@ JITSI_PRIVATE_MODE=false JITSI_ISS= SECRET_JITSI_KEY= ADMIN_API_TOKEN=123 -START_ROOM_URL=/_/global/maps.workadventure.localhost/starter/map.json -# If your Turn server is configured to use the Turn REST API, you should put the shared auth secret here. -# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file. -# Keep empty if you are sharing hard coded / clear text credentials. -TURN_STATIC_AUTH_SECRET= -DISABLE_NOTIFICATIONS=true -SKIP_RENDER_OPTIMIZATIONS=false - -# The email address used by Let's encrypt to send renewal warnings (compulsory) -ACME_EMAIL= - -MAX_PER_GROUP=4 -MAX_USERNAME_LENGTH=8 - -OPID_CLIENT_ID= -OPID_CLIENT_SECRET= -OPID_CLIENT_ISSUER= -OPID_CLIENT_REDIRECT_URL= -OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen -OPID_PROFILE_SCREEN_PROVIDER= -DISABLE_ANONYMOUS= - -# If you want to have a contact page in your menu, you MUST set CONTACT_URL to the URL of the page that you want -CONTACT_URL= \ No newline at end of file diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 9c91e0ca..e8c683b9 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -1,13 +1,7 @@ name: Build, push and deploy Docker image on: - push: - branches: [master, develop] - release: - types: [created] - pull_request: - types: [ labeled, synchronize ] - + - push # Enables BuildKit env: @@ -16,7 +10,7 @@ env: jobs: build-front: - if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} + runs-on: ubuntu-latest steps: @@ -26,7 +20,7 @@ jobs: # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@3.1.0 + - uses: rlespinasse/github-slug-action@1.1.1 - name: "Build and push front image" uses: docker/build-push-action@v1 @@ -36,11 +30,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} repository: thecodingmachine/workadventure-front - tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} + tags: ${{ env.GITHUB_REF_SLUG }} add_git_labels: true build-back: - if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} + runs-on: ubuntu-latest steps: @@ -49,7 +43,7 @@ jobs: uses: actions/checkout@v2 # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@3.1.0 + - uses: rlespinasse/github-slug-action@1.1.1 - name: "Build and push back image" uses: docker/build-push-action@v1 @@ -59,11 +53,11 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} repository: thecodingmachine/workadventure-back - tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} + tags: ${{ env.GITHUB_REF_SLUG }} add_git_labels: true - build-pusher: - if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} + build-website: + runs-on: ubuntu-latest steps: @@ -72,44 +66,21 @@ jobs: uses: actions/checkout@v2 # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@3.1.0 + - uses: rlespinasse/github-slug-action@1.1.1 - name: "Build and push back image" uses: docker/build-push-action@v1 with: - dockerfile: pusher/Dockerfile - path: ./ + dockerfile: website/Dockerfile + path: website/ username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - repository: thecodingmachine/workadventure-pusher - tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} - add_git_labels: true - - build-uploader: - if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} - runs-on: ubuntu-latest - - steps: - - - name: Checkout - uses: actions/checkout@v2 - - # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@3.1.0 - - - name: "Build and push back image" - uses: docker/build-push-action@v1 - with: - dockerfile: uploader/Dockerfile - path: ./ - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - repository: thecodingmachine/workadventure-uploader - tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} + repository: thecodingmachine/workadventure-website + tags: ${{ env.GITHUB_REF_SLUG }} add_git_labels: true build-maps: - if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} + runs-on: ubuntu-latest steps: @@ -119,7 +90,7 @@ jobs: # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@3.1.0 + - uses: rlespinasse/github-slug-action@1.1.1 - name: "Build and push front image" uses: docker/build-push-action@v1 @@ -129,76 +100,67 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} repository: thecodingmachine/workadventure-maps - tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} + tags: ${{ env.GITHUB_REF_SLUG }} add_git_labels: true deeploy: needs: - build-front - build-back - - build-pusher - - build-maps - - build-uploader runs-on: ubuntu-latest - if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }} steps: - name: Checkout uses: actions/checkout@v2 # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@3.1.0 - - - name: Write certificate - run: echo "${CERTS_PRIVATE_KEY}" > secret.key && chmod 0600 secret.key - env: - CERTS_PRIVATE_KEY: ${{ secrets.CERTS_PRIVATE_KEY }} - - - name: Download certificate - run: mkdir secrets && scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i secret.key ubuntu@cert.workadventu.re:./config/live/workadventu.re/* secrets/ - - - name: Create namespace - uses: steebchen/kubectl@v1.0.0 - env: - KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }} - with: - args: create namespace workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} - continue-on-error: true - - - name: Delete old certificates in namespace - uses: steebchen/kubectl@v1.0.0 - env: - KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }} - with: - args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} delete secret certificate-tls - continue-on-error: true - - - name: Install certificates in namespace - uses: steebchen/kubectl@v1.0.0 - env: - KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }} - with: - args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} create secret tls certificate-tls --key="secrets/privkey.pem" --cert="secrets/fullchain.pem" + - uses: rlespinasse/github-slug-action@1.1.0 - name: Deploy - uses: thecodingmachine/deeployer-action@master + uses: thecodingmachine/deeployer@master env: KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }} ADMIN_API_TOKEN: ${{ secrets.ADMIN_API_TOKEN }} JITSI_ISS: ${{ secrets.JITSI_ISS }} JITSI_URL: ${{ secrets.JITSI_URL }} SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }} - TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }} - DEPLOY_REF: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} - POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }} - POSTHOG_URL: ${{ secrets.POSTHOG_URL }} with: - namespace: workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} + namespace: workadventure-${{ env.GITHUB_REF_SLUG }} - name: Add a comment in PR uses: unsplash/comment-on-pr@v1.2.0 - if: ${{ github.event_name == 'pull_request' }} + if: ${{ env.GITHUB_REF_SLUG != 'master' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - msg: "Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re \nTests available at https://maps-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re/tests" + msg: Environment deployed at https://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com + check_for_duplicate_msg: true + + - name: Run Cypress tests + uses: cypress-io/github-action@v1 + if: ${{ env.GITHUB_REF_SLUG != 'master' }} + env: + CYPRESS_BASE_URL: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com + with: + env: host=play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com,port=80 + spec: cypress/integration/spec.js + wait-on: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com + working-directory: e2e + + - name: Run Cypress tests in prod + uses: cypress-io/github-action@v1 + if: ${{ env.GITHUB_REF_SLUG == 'master' }} + env: + CYPRESS_BASE_URL: https://play.workadventu.re + with: + env: host=play.workadventu.re + spec: cypress/integration/spec.js + wait-on: https://workadventu.re + working-directory: e2e + + - name: "Upload the screenshot on test failure" + uses: actions/upload-artifact@v1 + if: failure() + with: + name: "screenshot" + path: "./e2e/cypress/screenshots/spec.js/WorkAdventureGame -- loads (failed).png" diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index ffa8273f..bb6bcc89 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -1,8 +1,7 @@ name: Cleanup images and environments on: - pull_request: - types: [ closed ] + - delete # Enables BuildKit env: @@ -15,12 +14,13 @@ jobs: steps: # Create a slugified value of the branch - - uses: rlespinasse/github-slug-action@3.1.0 + - uses: rlespinasse/github-slug-action@1.1.0 - name: Cleanup - continue-on-error: true uses: thecodingmachine/deeployer-cleanup-action@master env: KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }} with: - namespace: workadventure-${{ env.GITHUB_HEAD_REF_SLUG }} +# FIXME: we are not using ${{ env.GITHUB_REF_SLUG }} that resolves to master BUT! we are not using a slugified namespace +# so complex namespace names will not be treated correctly + namespace: workadventure-${{ github.event.ref }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 3a2a562d..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,71 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ develop ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ develop ] - schedule: - - cron: '24 17 * * 0' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 45bcbfe0..47b28d72 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -3,13 +3,11 @@ name: "Continuous Integration" on: - push: - branches: - - master - - develop - pull_request: + - "pull_request" + - "push" jobs: + continuous-integration-front: name: "Continuous Integration Front" @@ -38,87 +36,23 @@ jobs: working-directory: "messages" - name: "Build proto messages" - run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front + run: yarn run proto && yarn run copy-to-front working-directory: "messages" - - name: "Create index.html" - run: ./templater.sh - working-directory: "front" - - - name: "Generate i18n files" - run: yarn run typesafe-i18n - working-directory: "front" - - name: "Build" run: yarn run build env: - PUSHER_URL: "//localhost:8080" - ADMIN_URL: "//localhost:80" - working-directory: "front" - - - name: "Svelte check" - run: yarn run svelte-check + API_URL: "localhost:8080" working-directory: "front" - name: "Lint" run: yarn run lint working-directory: "front" - - name: "Pretty" - run: yarn run pretty-check - working-directory: "front" - - name: "Jasmine" run: yarn test working-directory: "front" - continuous-integration-pusher: - name: "Continuous Integration Pusher" - - runs-on: "ubuntu-latest" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.0.0" - - - name: "Setup NodeJS" - uses: actions/setup-node@v1 - with: - node-version: '14.x' - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - version: '3.x' - - - name: "Install dependencies" - run: yarn install - working-directory: "pusher" - - - name: "Install messages dependencies" - run: yarn install - working-directory: "messages" - - - name: "Build proto messages" - run: yarn run proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher - working-directory: "messages" - - - name: "Build" - run: yarn run tsc - working-directory: "pusher" - - - name: "Lint" - run: yarn run lint - working-directory: "pusher" - - - name: "Jasmine" - run: yarn test - working-directory: "pusher" - - - name: "Prettier" - run: yarn run pretty-check - working-directory: "pusher" - continuous-integration-back: name: "Continuous Integration Back" @@ -162,7 +96,3 @@ jobs: run: yarn test working-directory: "back" - - name: "Prettier" - run: yarn run pretty-check - working-directory: "back" - diff --git a/.github/workflows/end_to_end_tests.yml b/.github/workflows/end_to_end_tests.yml deleted file mode 100644 index f9dc832c..00000000 --- a/.github/workflows/end_to_end_tests.yml +++ /dev/null @@ -1,126 +0,0 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - -name: "End to end tests" - -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - - start-runner: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) - name: Start self-hosted EC2 runner - runs-on: ubuntu-latest - outputs: - label: ${{ steps.start-ec2-runner.outputs.label }} - ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }} - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - - name: Start EC2 runner - id: start-ec2-runner - uses: machulav/ec2-github-runner@v2 - with: - mode: start - github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} - ec2-image-id: ami-094dbcc53250a2480 - ec2-instance-type: m5.2xlarge - subnet-id: subnet-0ac40025f559df1bc - security-group-id: sg-0e36e96e3b8ed2d64 - #iam-role-name: my-role-name # optional, requires additional permissions - #aws-resource-tags: > # optional, requires additional permissions - # [ - # {"Key": "Name", "Value": "ec2-github-runner"}, - # {"Key": "GitHubRepository", "Value": "${{ github.repository }}"} - # ] - - - end-to-end-tests: - name: "End-to-end testcafe tests" - - needs: start-runner # required to start the main job when the runner is ready - runs-on: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runner - - steps: - - name: "Checkout" - uses: "actions/checkout@v2.0.0" - - - name: "Setup NodeJS" - uses: actions/setup-node@v1 - with: - node-version: '14.x' - - - name: "Install dependencies" - run: npm install - working-directory: "tests" - - - name: "Setup .env file" - run: cp .env.template .env - - - name: "Edit ownership of file for test cases" - run: sudo chown 1000:1000 -R . - - - name: "Start environment" - run: LIVE_RELOAD=0 docker-compose up -d - - - name: "Wait for environment to build (and downloading testcafe image)" - run: (docker-compose -f docker-compose.testcafe.yml build &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully" - -# - name: "temp debug: display logs" -# run: docker-compose logs -# -# - name: "Wait for back start" -# run: docker-compose logs -f back | grep -q "WorkAdventure HTTP API starting on port" -# -# - name: "Wait for pusher start" -# run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port" - - - name: "Run tests" - run: PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe - - - name: Upload failed tests - if: ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: my-artifact - path: './tests/screenshots/' - - - name: Display state - if: ${{ failure() }} - run: docker-compose ps - - - name: Display logs - if: ${{ failure() }} - run: docker-compose logs - - stop-runner: - name: Stop self-hosted EC2 runner - needs: - - start-runner # required to get output from the start-runner job - - end-to-end-tests # required to wait when the main job is done - runs-on: ubuntu-latest - if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs - steps: - - name: Configure AWS credentials - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - - name: Stop EC2 runner - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) - uses: machulav/ec2-github-runner@v2 - with: - mode: stop - github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} - label: ${{ needs.start-runner.outputs.label }} - ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }} diff --git a/.github/workflows/push-to-npm.yml b/.github/workflows/push-to-npm.yml deleted file mode 100644 index 750ef224..00000000 --- a/.github/workflows/push-to-npm.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Push @workadventure/iframe-api-typings to NPM -on: - release: - types: [created] - push: -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - # Setup .npmrc file to publish to npm - - uses: actions/setup-node@v2 - with: - node-version: '14.x' - registry-url: 'https://registry.npmjs.org' - - - name: Replace version number - run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json' - working-directory: "front/packages/iframe-api-typings" - - - name: Debug package.json - run: cat package.json - working-directory: "front/packages/iframe-api-typings" - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - version: '3.x' - - - name: "Install dependencies" - run: yarn install - working-directory: "front" - - - name: "Install messages dependencies" - run: yarn install - working-directory: "messages" - - - name: "Build proto messages" - run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front - working-directory: "messages" - - - name: "Create index.html" - run: ./templater.sh - working-directory: "front" - - - name: "Generate i18n files" - run: yarn run typesafe-i18n - working-directory: "front" - - - name: "Build" - run: yarn run build-typings - env: - PUSHER_URL: "//localhost:8080" - ADMIN_URL: "//localhost:80" - working-directory: "front" - - # We build the front to generate the typings of iframe_api, then we copy those typings in a separate package. - - name: Copy typings to package dir - run: cp front/dist/src/iframe_api.d.ts front/packages/iframe-api-typings/iframe_api.d.ts - - - name: Copy typings to package dir (2) - run: cp -R front/dist/src/Api front/packages/iframe-api-typings/Api - - - name: Install dependencies in package - run: yarn install - working-directory: "front/packages/iframe-api-typings" - - - name: Publish package - run: yarn publish - working-directory: "front/packages/iframe-api-typings" - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - if: ${{ github.event_name == 'release' }} diff --git a/.gitignore b/.gitignore index 8fa69985..659dab86 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,4 @@ .vagrant Vagrantfile docker-compose.override.yaml -*.DS_Store -maps/yarn.lock -maps/dist/computer.js -maps/dist/computer.js.map -node_modules -_ \ No newline at end of file +*.DS_Store \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore deleted file mode 100644 index 31354ec1..00000000 --- a/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 8fa7767b..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -( -cd messages || exit -yarn run precommit -) -( -cd front || exit -yarn run precommit -) -( -cd pusher || exit -yarn run precommit -) -( -cd back || exit -yarn run precommit -) diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e33a4f97..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,166 +0,0 @@ -## Version develop - -### Updates -- Added multi Co-Website management - -### Bugfix -- Moving a discussion over a user will now add this user to the discussion -- Being in a silent zone new forces mediaConstraints to false (#1508) -- Fixes for the emote menu (#1501) -- Fixing chat message attributed to wrong user (#1507 #1528) - -## Version 1.5.0 -### Updates -- Added support for login with OpenID Connect -- New scripting library available to extend WorkAdventure: see [Scripting API Extra](https://github.com/workadventure/scripting-api-extra/) -- New menu design! -- New `openTab` property (#1419) -- Possible integration with Posthog (#1458) - -### Bugfix -- Fixing layers flattened several times (#1427 @Lurkars) -- Fixing CSS of video elements -- Chat now scrolls to bottom when opened (#1450) -- Fixing silent zone not respected when exiting from Jitsi (#1456) -- Fixing "yarn install" failing because of missing rights on some Docker installs (#1457) -- Fixing audio not shut down when exiting a room (#1459) - -### Misc -- Finished migrating "Build your map" documentation into the "/docs" directory of this repository (#1417 #1385) -- Refactoring documentation (dedicated page for variables) (#1414) -- Front container code is now completely linted (#1413) - -## Version 1.4.15 - -### Updates -- New scripting API features : - - Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu` to add a custom menu or an iframe to the menu. -- New `jitsiWidth` parameter to set the width of Jitsi and Cowebsite (#1398 @tabascoeye) -- Refactored the way videos are displayed to better cope for vertical videos (on mobile) -- Fixing reconnection issues after 5 minutes of an inactive tab on Google Chrome -- Changes performed in `WA.room.setPropertyLayer` now have a real-time impact (#1395) - -### Bugfixes -- Fixing streams in bubbles sometimes improperly muted when there are more than 2 people in the bubble (#1400 #1402) -- Properly displaying carriage returns in popups (#1388) -- `WA.state` now answers correctly to "in" keyword (#1393) -- Variables can now be nested in group layers (#1406) - -## Version 1.4.14 - -### Updates -- New scripting API features : - - Use `WA.room.loadTileset(url: string) : Promise` to load a tileset from a JSON file. -- Rewrote the way authentification works: the auth jwt token can now contains an email instead of an uuid -- Added an OpenId login flow than can be plugged to any OIDC provider. -- You can send a message to all rooms of your world from the console global message (user with tag admin only). - -## Version 1.4.11 - -### Updates - -- Added the ability to have animated tiles in maps #1216 #1217 -- Enabled outlines on actionable item again (they were disabled when migrating to Phaser 3.50) #1218 -- Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219 -- Migrated the admin console to Svelte, and redesigned the console #1211 -- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1) -- New scripting API features : - - Use `WA.onInit(): Promise` to wait for scripting API initialization - - Use `WA.room.showLayer(): void` to show a layer - - Use `WA.room.hideLayer(): void` to hide a layer - - Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer - - Use `WA.player.onPlayerMove(): void` to track the movement of the current player - - Use `WA.player.id: string|undefined` to get the ID of the current player - - Use `WA.player.name: string` to get the name of the current player - - Use `WA.player.tags: string[]` to get the tags of the current player - - Use `WA.room.id: string` to get the ID of the room - - Use `WA.room.mapURL: string` to get the URL of the map - - Use `WA.room.mapURL: string` to get the URL of the map - - Use `WA.room.getMap(): Promise` to get the JSON map file - - Use `WA.room.setTiles(): void` to add, delete or change an array of tiles - - Use `WA.ui.registerMenuCommand(): void` to add a custom menu - - Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable - - Use `WA.state.saveVariable(key: string, value: unknown): Promise` to set a variable (across the room, for all users) - - Use `WA.state.onVariableChange(key: string): Observable` to track a variable - - Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`) -- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked. -- The text chat was redesigned to be prettier and to use more features : - - The chat is now persistent between discussions and always accessible - - The chat now tracks incoming and outcoming users in your conversation - - The chat allows your to see the visit card of users - - You can close the chat window with the escape key -- Added a 'Enable notifications' button in the menu. -- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed: - - `/api/map`: now accepts a complete room URL instead of organization/world/room slugs - - `/api/ban`: new endpoint to report users - - as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing - -## Version 1.4.3 - 1.4.4 - 1.4.5 - -## Bugfixes - -- Fixing the generation of @workadventure/iframe-api-typings - -## Version 1.4.2 - -## Updates - -- A script in an iframe opened by another script can use the IFrame API. - -## Version 1.4.1 - -### Bugfixes - -- Loading errors after the preload stage should not crash the game anymore - -## Version 1.4.0 - -### BREAKING CHANGES - -- Scripting API: - - Changed function names: `restorePlayerControl` => `restorePlayerControls`, `disablePlayerControl` => `disablePlayerControls`. - Please keep in mind that the scripting API is still experimental. Some breaking changes can occur in it until we mark it as stable. - -### Updates - -- Added the emote feature to WorkAdventure. (@Kharhamel, @Tabascoeye) - - The emote menu can be opened by clicking on your character. - - Clicking on one of its element will close the menu and play an emote above your character. - - This emote can be seen by other players. -- Player names were improved. (@Kharhamel) - - We now create a GameObject.Text instead of GameObject.BitmapText - - now use the 'Press Start 2P' font family and added an outline - - As a result, we can now allow non-standard letters like french accents or chinese characters! - -- Added the contact card feature. (@Kharhamel) - - Click on another player to see its contact info. - - Premium-only feature unfortunately. I need to find a way to make it available for all. - - If no contact data is found (either because the user is anonymous or because no admin backend), display an error card. - -- Mobile support has been improved - - WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used - - Mouse wheel support to zoom in / out - - Pinch support on mobile to zoom in / out - - Improved virtual joystick size (adapts to the zoom level) -- Redesigned intermediate scenes - - Redesigned Select Companion scene - - Redesigned Enter Your Name scene - - Added a new `DISPLAY_TERMS_OF_USE` environment variable to trigger the display of terms of use -- New scripting API features: - - Use `WA.loadSound(): Sound` to load / play / stop a sound - - -### Bug Fixes - -- Pinch gesture does no longer move the character - -## Version 1.3.0 - -### New Features - -* Maps can now contain "group" layers (layers that contain other layers) - #899 #779 (@Lurkars @moufmouf) - -### Updates - - -### Bug Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 2d3f5d0b..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,113 +0,0 @@ -# Contributing to WorkAdventure - -Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to -ask questions and how to work on something. - -## Contributions we are seeking - -We love to receive contributions from our community — you! - -There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, -submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself. - -## Contributing external resources - -You can share your work on maps / articles / videos related to WorkAdventure on our [awesome-workadventure](https://github.com/workadventure/awesome-workadventure) list. - -## Developer documentation - -Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/) - -## Using the issue tracker - -First things first: **Do NOT report security vulnerabilities in public issues!**. -Please read the [security guide](SECURITY.md) to learn who to do a security disclosure to the WorkAdventure core team. - -You can use [GitHub issue tracker](https://github.com/thecodingmachine/workadventure/issues) to: - -- File bug reports -- Ask for feature requests - -If you have more general questions, a good place to ask is [our Discord server](https://discord.gg/YGtngdh9gt). - -Finally, you can come and talk to the WorkAdventure core team... on WorkAdventure, of course! [Our offices are here](https://play.staging.workadventu.re/@/tcm/workadventure/wa-village). - -## Pull requests - -Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope -and avoid containing unrelated commits. - -Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), -otherwise you risk spending a lot of time working on something that the project's developers might not want to merge -into the project. - -You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues). - -### Linting your code - -Before committing, be sure to install the "Prettier" precommit hook that will reformat your code to our coding style. - -In order to enable the "Prettier" precommit hook, at the root of the project, run: - -```console -$ yarn install -$ yarn run prepare -``` - -If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need -to run code linting manually: - -```console -$ docker-compose exec front yarn run pretty -$ docker-compose exec pusher yarn run pretty -$ docker-compose exec back yarn run pretty -``` - -### Providing tests - -WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test. - -Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine), or an end-to-end test (we use Testcafe). - -If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain -some description text describing how to test the feature. - -* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference - to your newly created test map -* if the features can be automatically tested, please provide a testcafe test - -#### Running testcafe tests - -End-to-end tests are available in the "/tests" directory. - -To run these tests locally: - -```console -$ LIVE_RELOAD=0 docker-compose up -d -$ cd tests -$ npm install -$ npm run test -``` - -Note: If your tests fail on a Javascript error in "sockjs", this is due to the -Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting -WorkAdventure with the `LIVE_RELOAD=0` environment variable. - -End-to-end tests can take a while to run. To run only one test, use: - -```console -$ npm run test -- tests/[name of the test file].ts -``` - -You can also run the tests inside a container (but you will not have visual feedbacks on your test, so we recommend using -the local tests). - -```console -$ LIVE_RELOAD=0 docker-compose up -d -# Wait 2-3 minutes for the environment to start, then: -$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up -``` - -### A bad wording or a missing language - -If you notice a translation error or missing language you can help us by following the [how to translate](docs/dev/how-to-translate.md) documentation. diff --git a/README-INTRO.jpg b/README-INTRO.jpg new file mode 100644 index 00000000..989b8e78 Binary files /dev/null and b/README-INTRO.jpg differ diff --git a/README-LOGO.svg b/README-LOGO.svg deleted file mode 100644 index f66f0603..00000000 --- a/README-LOGO.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/README-MAP.png b/README-MAP.png deleted file mode 100644 index a592d222..00000000 Binary files a/README-MAP.png and /dev/null differ diff --git a/README.md b/README.md index 21871991..faafed98 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,41 @@ -![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/YGtngdh9gt) +![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) -![WorkAdventure logo](README-LOGO.svg) -![WorkAdventure office image](README-MAP.png) +![WorkAdventure landscape image](README-INTRO.jpg) -Live demo [here](https://play.workadventu.re/@/tcm/workadventure/wa-village). +Demo here : [https://workadventu.re/](https://workadventu.re/). -# WorkAdventure +# Work Adventure -WorkAdventure is a web-based collaborative workspace presented in the form of a +## Work in progress + +Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a 16-bit video game. -In WorkAdventure you can move around your office and talk to your colleagues (using a video-chat system, triggered when you approach someone). +In Work Adventure, you can move around your office and talk to your colleagues (using a video-chat feature that is +triggered when you move next to a colleague). -See more features for your virtual office: https://workadventu.re/virtual-office -## Community resources - -Check out resources developed by the WorkAdventure community at [awesome-workadventure](https://github.com/workadventure/awesome-workadventure) - -## Setting up a development environment +## Getting started Install Docker. Run: ``` -cp .env.template .env -docker-compose up -d +docker-compose up ``` The environment will start. -You should now be able to browse to http://play.workadventure.localhost/ and see the application. -You can view the dashboard at http://workadventure.localhost:8080/ +You should now be able to browse to http://workadventure.localhost/ and see the application. Note: on some OSes, you will need to add this line to your `/etc/hosts` file: **/etc/hosts** ``` -127.0.0.1 workadventure.localhost +workadventure.localhost 127.0.0.1 ``` -Note: If on the first run you get a page with "network error". Try to ``docker-compose stop`` , then ``docker-compose start``. -Note 2: If you are still getting "network error". Make sure you are authorizing the self-signed certificate by entering https://pusher.workadventure.localhost and accepting them. - ### MacOS developers, your environment with Vagrant If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c). @@ -109,7 +101,5 @@ Vagrant destroy * `Vagrant halt`: stop your VM Vagrant. * `Vagrant destroy`: delete your VM Vagrant. -## Setting up a production environment - -The way you set up your production environment will highly depend on your servers. -We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) directory. +## Features developed +You have more details of features developed in back [README.md](./back/README.md). diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 562da3e0..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,20 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability - -First things first: **Do NOT report security vulnerabilities in public issues!** - -Please disclose responsibly by sending -a mail at security@workadventu.re (you can also ping us in the GitHub issues, but please, no details in the issues!) - -We will assess the issue as soon as possible on a best-effort basis and will give you an estimate for when we have a fix -and release available for an eventual public disclosure. - -We do not have a bug bounty program. - -## Supported Versions - -We only apply security patches on the latest tagged release and on the `master` and `develop` branches - -Unless specified otherwise, do not expect us to fix security issues on past releases. We are only maintaining one release: -the latest one, which is online at https://play.workadventu.re. diff --git a/Vagrantfile.template b/Vagrantfile.template index 14d30525..738e24b7 100644 --- a/Vagrantfile.template +++ b/Vagrantfile.template @@ -2,7 +2,7 @@ # -*- mode: ruby -*- # vi: set ft=ruby : # Box / OS -VAGRANT_BOX = 'bento/ubuntu-20.04' +VAGRANT_BOX = 'bento/ubuntu-19.10' # VM User — 'vagrant' by default VM_USER = 'vagrant' @@ -58,7 +58,7 @@ Vagrant.configure(2) do |config| apt-get update -y apt-get install -y git apt-get install -y apt-transport-https - apt-get install -y ca-certificates + apt-get install -y build-essential apt-get install -y curl apt-get install -y gnupg-agent apt-get install -y software-properties-common @@ -66,8 +66,8 @@ Vagrant.configure(2) do |config| apt-key fingerprint 0EBFCD88 add-apt-repository \ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) \ - stable" + $(lsb_release -cs) \ + stable" apt-get update -y apt-get install -y docker-ce docker-ce-cli containerd.io curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose diff --git a/back/.eslintrc.json b/back/.eslintrc.json index ce78dd63..3aab37d9 100644 --- a/back/.eslintrc.json +++ b/back/.eslintrc.json @@ -25,7 +25,6 @@ ], "rules": { "no-unused-vars": "off", - "@typescript-eslint/no-explicit-any": "error", - "no-throw-literal": "error" + "@typescript-eslint/no-explicit-any": "error" } } diff --git a/back/.prettierignore b/back/.prettierignore deleted file mode 100644 index 1f453464..00000000 --- a/back/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -src/Messages/generated diff --git a/back/.prettierrc.json b/back/.prettierrc.json deleted file mode 100644 index e8980d15..00000000 --- a/back/.prettierrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "printWidth": 120, - "tabWidth": 4 -} diff --git a/back/Dockerfile b/back/Dockerfile index a8ce3c1c..02369b9f 100644 --- a/back/Dockerfile +++ b/back/Dockerfile @@ -1,26 +1,15 @@ -# protobuf build -FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder -WORKDIR /usr/src -COPY messages . +FROM thecodingmachine/workadventure-back-base:latest as builder +WORKDIR /var/www/messages +COPY --chown=docker:docker messages . RUN yarn install && yarn proto -# typescript build -FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder2 -WORKDIR /usr/src -COPY back/yarn.lock back/package.json ./ +FROM thecodingmachine/nodejs:12 + +COPY --chown=docker:docker back . +COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated RUN yarn install -COPY back . -COPY --from=builder /usr/src/generated src/Messages/generated -ENV NODE_ENV=production -RUN yarn run tsc -# final production image -FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d -WORKDIR /usr/src -COPY back/yarn.lock back/package.json ./ -COPY --from=builder2 /usr/src/dist /usr/src/dist ENV NODE_ENV=production -RUN yarn install --production -USER node -CMD ["yarn", "run", "runprod"] +CMD ["yarn", "run", "prod"] + diff --git a/back/README.md b/back/README.md new file mode 100644 index 00000000..8a78f403 --- /dev/null +++ b/back/README.md @@ -0,0 +1,61 @@ +# Back Features + +## Login +To start your game, you must authenticate on the server back. +When you are authenticated, the back server return token and room starting. +``` +POST => /login +Params : + email: email of user. +``` + +## Join a room +When a user is connected, the user can join a room. +So you must send emit `join-room` with information user: +``` +Socket.io => 'join-room' + + userId: user id of gamer + roomId: room id when user enter in game + position: { + x: position x on map + y: position y on map + } +``` +All data users are stocked on socket client. + +## Send position user +When user move on the map, you can share new position on back with event `user-position`. +The information sent: +``` +Socket.io => 'user-position' + + userId: user id of gamer + roomId: room id when user enter in game + position: { + x: position x on map + y: position y on map + } +``` +All data users are updated on socket client. + +## Receive positions of all users +The application sends position of all users in each room in every few 10 milliseconds. +The data will pushed on event `user-position`: +``` +Socket.io => 'user-position' + + [ + { + userId: user id of gamer + roomId: room id when user enter in game + position: { + x: position x on map + y: position y on map + } + }, + ... + ] +``` + +[<<< back](../README.md) \ No newline at end of file diff --git a/back/package.json b/back/package.json index f5ba46c7..bb34e186 100644 --- a/back/package.json +++ b/back/package.json @@ -7,14 +7,10 @@ "tsc": "tsc", "dev": "ts-node-dev --respawn ./server.ts", "prod": "tsc && node --max-old-space-size=4096 ./dist/server.js", - "runprod": "node --max-old-space-size=4096 ./dist/server.js", "profile": "tsc && node --prof ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", - "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", - "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", - "precommit": "lint-staged", - "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", - "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" + "lint": "node_modules/.bin/eslint src/ . --ext .ts", + "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts" }, "repository": { "type": "git", @@ -40,46 +36,37 @@ }, "homepage": "https://github.com/thecodingmachine/workadventure#readme", "dependencies": { - "@workadventure/tiled-map-type-guard": "^1.0.3", - "axios": "^0.21.2", + "axios": "^0.20.0", + "body-parser": "^1.19.0", "busboy": "^0.3.1", "circular-json": "^0.5.9", - "debug": "^4.3.1", "generic-type-guard": "^3.2.0", "google-protobuf": "^3.13.0", - "grpc": "^1.24.4", - "ipaddr.js": "^2.0.1", + "http-status-codes": "^1.4.0", + "iterall": "^1.3.0", "jsonwebtoken": "^8.5.1", "mkdirp": "^1.0.4", + "multer": "^1.4.2", "prom-client": "^12.0.0", "query-string": "^6.13.3", - "redis": "^3.1.2", + "systeminformation": "^4.27.11", + "ts-node-dev": "^1.0.0-pre.44", + "typescript": "^3.8.3", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", "uuidv4": "^6.0.7" }, "devDependencies": { "@types/busboy": "^0.2.3", "@types/circular-json": "^0.4.0", - "@types/debug": "^4.1.5", "@types/google-protobuf": "^3.7.3", "@types/http-status-codes": "^1.2.0", "@types/jasmine": "^3.5.10", "@types/jsonwebtoken": "^8.3.8", "@types/mkdirp": "^1.0.1", - "@types/redis": "^2.8.31", "@types/uuidv4": "^5.0.0", - "@typescript-eslint/eslint-plugin": "^5.8.0", - "@typescript-eslint/parser": "^5.8.0", - "eslint": "^8.5.0", - "jasmine": "^3.5.0", - "lint-staged": "^11.0.0", - "prettier": "^2.3.1", - "ts-node-dev": "^1.1.8", - "typescript": "^4.5.4" - }, - "lint-staged": { - "*.ts": [ - "prettier --write" - ] + "@typescript-eslint/eslint-plugin": "^2.26.0", + "@typescript-eslint/parser": "^2.26.0", + "eslint": "^6.8.0", + "jasmine": "^3.5.0" } } diff --git a/back/position-test.js b/back/position-test.js new file mode 100644 index 00000000..01fb5cdf --- /dev/null +++ b/back/position-test.js @@ -0,0 +1,148 @@ +// Constants +let MIN_DISTANCE = 12; +let MAX_PER_GROUP = 3; +let NB_USERS = 10; + +// Utils +let rand = function(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +let compareDistances = function(distA, distB) { + if (distA.distance < distB.distance) { + return -1; + } + if (distA.distance > distB.distance) { + return 1; + } + return 0; +}; + +let computeDistance = function (user1, user2) { + return Math.sqrt(Math.pow(user2.X - user1.X, 2) + Math.pow(user2.Y - user1.Y, 2)); +}; + +// Test Data +let users = []; +for(let i = 1; i <= NB_USERS; i++) { + let user = {}; + user.id = rand(0,99999); + user.X = rand(0, 40); + user.Y = rand(0, 40); + users.push(user); +} + +// Compute distance between each user +let getDistanceOfEachUser = function(users) { + let i = 0; + let distances = []; + + users.forEach(function(user1, key1) { + users.forEach(function(user2, key2) { + if(key1 < key2) { + let distanceObj = {}; + distanceObj.distance = computeDistance(user1, user2); + distanceObj.first = user1; + distanceObj.second = user2; + + distances[i] = distanceObj; + i++; + } + }); + }); + + return distances; +}; + +// Organise groups +let createGroups = function(distances) { + let i = 0; + let groups = []; + let alreadyInAGroup = []; + + for(let j = 0; j < distances.length; j++) { + let dist = distances[j]; + + if(dist.distance <= MIN_DISTANCE) { + if(typeof groups[i] === 'undefined') { + groups[i] = []; + } + + if(groups[i].indexOf(dist.first) === -1 && typeof alreadyInAGroup[dist.first.id] === 'undefined') { + if(groups[i].length > 1) { + // if group is not empty we check current user can be added in the group according to its distance to the others already in it + for(let l = 0; l < groups[i].length; l++) { + let userTotest = groups[i][l]; + if(computeDistance(dist.first, userTotest) <= MIN_DISTANCE) { + groups[i].push(dist.first); + alreadyInAGroup[dist.first.id] = true; + break; + } + } + } else { + groups[i].push(dist.first); + alreadyInAGroup[dist.first.id] = true; + } + } + + if(groups[i].length === MAX_PER_GROUP) { + i++; // on crÊÊ un nouveau groupe + if(i > (NB_USERS / MAX_PER_GROUP)) { + console.log('There is no room left for user ID : ' + dist.second.id + ' !'); + break; + } + continue; + } + + if(groups[i].indexOf(dist.second) === -1 && typeof alreadyInAGroup[dist.second.id] === 'undefined') { + if(groups[i].length > 1) { + // if group is not empty we check current user can be added in the group according to its distance to the others already in it + for(let l = 0; l < groups[i].length; l++) { + let userTotest = groups[i][l]; + if(computeDistance(dist.second, userTotest) <= MIN_DISTANCE) { + groups[i].push(dist.second); + alreadyInAGroup[dist.second.id] = true; + break; + } + } + } else { + groups[i].push(dist.second); + alreadyInAGroup[dist.second.id] = true; + } + } + } + } + + return groups; +}; + +let distances = getDistanceOfEachUser(users); + +// ordonner par distance pour prioriser l'association en groupe des utilisateurs les plus proches +distances.sort(compareDistances); + +let groups = createGroups(distances); + +// Compute distance between each user of a already existing group +let checkGroupDistance = function(groups) { + for(let i = 0; i < groups.length; i++) { + let group = groups[i]; + group.forEach(function(user1, key1) { + group.forEach(function(user2, key2) { + if(key1 < key2) { + let distance = computeDistance(user1, user2); + if(distance > MIN_DISTANCE) { + // TODO : message a user1 et user2 + } + } + }); + }); + } +}; + +console.log(users); +console.log(distances); +console.log(groups); + diff --git a/back/server.ts b/back/server.ts index 1e474ec9..cb4a7604 100644 --- a/back/server.ts +++ b/back/server.ts @@ -1,15 +1,3 @@ // lib/server.ts import App from "./src/App"; -import grpc from "grpc"; -import { roomManager } from "./src/RoomManager"; -import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb"; -import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable"; - -App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT)); - -const server = new grpc.Server(); -server.addService(RoomManagerService, roomManager); - -server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure()); -server.start(); -console.log("WorkAdventure HTTP/2 API starting on port %d!", GRPC_PORT); +App.listen(8080, () => console.log(`WorkAdventure starting on port 8080!`)) diff --git a/back/src/App.ts b/back/src/App.ts index a6c42abb..42659aad 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -1,16 +1,29 @@ // lib/app.ts -import { PrometheusController } from "./Controller/PrometheusController"; -import { DebugController } from "./Controller/DebugController"; -import { App as uwsApp } from "./Server/sifrr.server"; +import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..." +import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..." +import {MapController} from "./Controller/MapController"; +import {PrometheusController} from "./Controller/PrometheusController"; +import {FileController} from "./Controller/FileController"; +import {DebugController} from "./Controller/DebugController"; +import {App as uwsApp} from "./Server/sifrr.server"; class App { public app: uwsApp; + public ioSocketController: IoSocketController; + public authenticateController: AuthenticateController; + public fileController: FileController; + public mapController: MapController; public prometheusController: PrometheusController; private debugController: DebugController; constructor() { this.app = new uwsApp(); + //create socket controllers + this.ioSocketController = new IoSocketController(this.app); + this.authenticateController = new AuthenticateController(this.app); + this.fileController = new FileController(this.app); + this.mapController = new MapController(this.app); this.prometheusController = new PrometheusController(this.app); this.debugController = new DebugController(this.app); } diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts new file mode 100644 index 00000000..bf68768d --- /dev/null +++ b/back/src/Controller/AuthenticateController.ts @@ -0,0 +1,135 @@ +import { v4 } from 'uuid'; +import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; +import {BaseController} from "./BaseController"; +import {adminApi} from "../Services/AdminApi"; +import {jwtTokenManager} from "../Services/JWTTokenManager"; +import {parse} from "query-string"; + +export interface TokenInterface { + userUuid: string +} + +export class AuthenticateController extends BaseController { + + constructor(private App : TemplatedApp) { + super(); + this.register(); + this.verify(); + this.anonymLogin(); + } + + //Try to login with an admin token + private register(){ + this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { + this.addCorsHeaders(res); + + res.end(); + }); + + this.App.post("/register", (res: HttpResponse, req: HttpRequest) => { + (async () => { + res.onAborted(() => { + console.warn('Login request was aborted'); + }) + const param = await res.json(); + + //todo: what to do if the organizationMemberToken is already used? + const organizationMemberToken:string|null = param.organizationMemberToken; + + try { + if (typeof organizationMemberToken != 'string') throw new Error('No organization token'); + const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); + const userUuid = data.userUuid; + const organizationSlug = data.organizationSlug; + const worldSlug = data.worldSlug; + const roomSlug = data.roomSlug; + const mapUrlStart = data.mapUrlStart; + const textures = data.textures; + + const authToken = jwtTokenManager.createJWTToken(userUuid); + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ + authToken, + userUuid, + organizationSlug, + worldSlug, + roomSlug, + mapUrlStart, + textures + })); + + } catch (e) { + console.error("An error happened", e) + res.writeStatus(e.status || "500 Internal Server Error"); + this.addCorsHeaders(res); + res.end('An error happened'); + } + + + })(); + }); + + } + + private verify(){ + this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => { + this.addCorsHeaders(res); + + res.end(); + }); + + this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => { + (async () => { + const query = parse(req.getQuery()); + + res.onAborted(() => { + console.warn('verify request was aborted'); + }) + + try { + await jwtTokenManager.getUserUuidFromToken(query.token as string); + } catch (e) { + res.writeStatus("400 Bad Request"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ + "success": false, + "message": "Invalid JWT token" + })); + return; + } + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ + "success": true + })); + })(); + }); + + } + + //permit to login on application. Return token to connect on Websocket IO. + private anonymLogin(){ + this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + this.addCorsHeaders(res); + + res.end(); + }); + + this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + + res.onAborted(() => { + console.warn('Login request was aborted'); + }) + + const userUuid = v4(); + const authToken = jwtTokenManager.createJWTToken(userUuid); + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify({ + authToken, + userUuid, + })); + }); + } +} diff --git a/back/src/Controller/BaseController.ts b/back/src/Controller/BaseController.ts index dc510d6c..0b744082 100644 --- a/back/src/Controller/BaseController.ts +++ b/back/src/Controller/BaseController.ts @@ -1,9 +1,11 @@ -import { HttpResponse } from "uWebSockets.js"; +import {HttpRequest, HttpResponse} from "uWebSockets.js"; +import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; + export class BaseController { protected addCorsHeaders(res: HttpResponse): void { - res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept"); - res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE"); - res.writeHeader("access-control-allow-origin", "*"); + res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept'); + res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); + res.writeHeader('access-control-allow-origin', '*'); } } diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index f571d6b2..af2db139 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -1,68 +1,45 @@ -import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; -import { stringify } from "circular-json"; -import { HttpRequest, HttpResponse } from "uWebSockets.js"; -import { parse } from "query-string"; -import { App } from "../Server/sifrr.server"; -import { socketManager } from "../Services/SocketManager"; +import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; +import {IoSocketController} from "_Controller/IoSocketController"; +import {stringify} from "circular-json"; +import {HttpRequest, HttpResponse} from "uWebSockets.js"; +import { parse } from 'query-string'; +import {App} from "../Server/sifrr.server"; +import {socketManager} from "../Services/SocketManager"; export class DebugController { - constructor(private App: App) { + constructor(private App : App) { this.getDump(); } - getDump() { + + getDump(){ this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { - (async () => { - const query = parse(req.getQuery()); + const query = parse(req.getQuery()); - if (ADMIN_API_TOKEN === "") { - return res.writeStatus("401 Unauthorized").end("No token configured!"); - } - if (query.token !== ADMIN_API_TOKEN) { - return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); - } + if (query.token !== ADMIN_API_TOKEN) { + return res.status(401).send('Invalid token sent!'); + } - return res - .writeStatus("200 OK") - .writeHeader("Content-Type", "application/json") - .end( - stringify( - await Promise.all(socketManager.getWorlds().values()), - (key: unknown, value: unknown) => { - if (key === "listeners") { - return "Listeners"; - } - if (key === "socket") { - return "Socket"; - } - if (key === "batchedMessages") { - return "BatchedMessages"; - } - if (value instanceof Map) { - const obj: { [key: string | number]: unknown } = {}; - for (const [mapKey, mapValue] of value.entries()) { - if (typeof mapKey === "number" || typeof mapKey === "string") { - obj[mapKey] = mapValue; - } - } - return obj; - } else if (value instanceof Set) { - const obj: Array = []; - for (const [setKey, setValue] of value.entries()) { - obj.push(setValue); - } - return obj; - } else { - return value; - } + return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( + socketManager.getWorlds(), + (key: unknown, value: unknown) => { + if(value instanceof Map) { + const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any + for (const [mapKey, mapValue] of value.entries()) { + obj[mapKey] = mapValue; + } + return obj; + } else if(value instanceof Set) { + const obj: Array = []; + for (const [setKey, setValue] of value.entries()) { + obj.push(setValue); } - ) - ); - })().catch((e) => { - console.error(e); - res.writeStatus("500"); - res.end("An error occurred"); - }); + return obj; + } else { + return value; + } + } + )); }); } } diff --git a/uploader/src/Controller/FileController.ts b/back/src/Controller/FileController.ts similarity index 98% rename from uploader/src/Controller/FileController.ts rename to back/src/Controller/FileController.ts index 08be3ee7..ae914aa8 100644 --- a/uploader/src/Controller/FileController.ts +++ b/back/src/Controller/FileController.ts @@ -105,7 +105,7 @@ export class FileController extends BaseController { this.App.get("/download-audio-message/:id", (res: HttpResponse, req: HttpRequest) => { res.onAborted(() => { - console.warn('download-audio-message request was aborted'); + console.warn('upload-audio-message request was aborted'); }) const id = req.getParameter(0); diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts new file mode 100644 index 00000000..0ae6465f --- /dev/null +++ b/back/src/Controller/IoSocketController.ts @@ -0,0 +1,325 @@ +import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." +import {GameRoomPolicyTypes} from "../Model/GameRoom"; +import {PointInterface} from "../Model/Websocket/PointInterface"; +import { + SetPlayerDetailsMessage, + SubMessage, + BatchMessage, + ItemEventMessage, + ViewportMessage, + ClientToServerMessage, + SilentMessage, + WebRtcSignalToServerMessage, + PlayGlobalMessage, + ReportPlayerMessage, + QueryJitsiJwtMessage +} from "../Messages/generated/messages_pb"; +import {UserMovesMessage} from "../Messages/generated/messages_pb"; +import {TemplatedApp} from "uWebSockets.js" +import {parse} from "query-string"; +import {jwtTokenManager} from "../Services/JWTTokenManager"; +import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; +import {SocketManager, socketManager} from "../Services/SocketManager"; +import {emitInBatch, resetPing} from "../Services/IoSocketHelpers"; +import {clientEventsEmitter} from "../Services/ClientEventsEmitter"; +import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; + +export class IoSocketController { + private nextUserId: number = 1; + + constructor(private readonly app: TemplatedApp) { + this.ioConnection(); + this.adminRoomSocket(); + } + + adminRoomSocket() { + this.app.ws('/admin/rooms', { + upgrade: (res, req, context) => { + const query = parse(req.getQuery()); + const websocketKey = req.getHeader('sec-websocket-key'); + const websocketProtocol = req.getHeader('sec-websocket-protocol'); + const websocketExtensions = req.getHeader('sec-websocket-extensions'); + const token = query.token; + if (token !== ADMIN_API_TOKEN) { + console.log('Admin access refused for token: '+token) + res.writeStatus("401 Unauthorized").end('Incorrect token'); + } + const roomId = query.roomId as string; + + res.upgrade( + {roomId}, + websocketKey, websocketProtocol, websocketExtensions, context, + ); + }, + open: (ws) => { + console.log('Admin socket connect for room: '+ws.roomId); + ws.send('Data:'+JSON.stringify(socketManager.getAdminSocketDataFor(ws.roomId as string))); + ws.clientJoinCallback = (clientUUid: string, roomId: string) => { + const wsroomId = ws.roomId as string; + if(wsroomId === roomId) { + ws.send('MemberJoin:'+clientUUid+';'+roomId); + } + }; + ws.clientLeaveCallback = (clientUUid: string, roomId: string) => { + const wsroomId = ws.roomId as string; + if(wsroomId === roomId) { + ws.send('MemberLeave:'+clientUUid+';'+roomId); + } + }; + clientEventsEmitter.registerToClientJoin(ws.clientJoinCallback); + clientEventsEmitter.registerToClientLeave(ws.clientLeaveCallback); + }, + message: (ws, arrayBuffer, isBinary): void => { + try { + //TODO refactor message type and data + const message: {event: string, message: {type: string, message: unknown, userUuid: string}} = + JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer))); + + if(message.event === 'user-message') { + const messageToEmit = (message.message as { message: string, type: string, userUuid: string }); + switch (message.message.type) { + case 'ban': { + socketManager.emitSendUserMessage(messageToEmit); + break; + } + case 'banned': { + const socketUser = socketManager.emitSendUserMessage(messageToEmit); + setTimeout(() => { + socketUser.close(); + }, 10000); + break; + } + default: { + break; + } + } + } + }catch (err) { + console.error(err); + } + }, + close: (ws, code, message) => { + //todo make sure this code unregister the right listeners + clientEventsEmitter.unregisterFromClientJoin(ws.clientJoinCallback); + clientEventsEmitter.unregisterFromClientLeave(ws.clientLeaveCallback); + } + }) + } + + ioConnection() { + this.app.ws('/room', { + /* Options */ + //compression: uWS.SHARED_COMPRESSOR, + maxPayloadLength: 16 * 1024 * 1024, + maxBackpressure: 65536, // Maximum 64kB of data in the buffer. + //idleTimeout: 10, + upgrade: (res, req, context) => { + //console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!'); + (async () => { + /* Keep track of abortions */ + const upgradeAborted = {aborted: false}; + + res.onAborted(() => { + /* We can simply signal that we were aborted */ + upgradeAborted.aborted = true; + }); + + try { + const url = req.getUrl(); + const query = parse(req.getQuery()); + const websocketKey = req.getHeader('sec-websocket-key'); + const websocketProtocol = req.getHeader('sec-websocket-protocol'); + const websocketExtensions = req.getHeader('sec-websocket-extensions'); + + const roomId = query.roomId; + if (typeof roomId !== 'string') { + throw new Error('Undefined room ID: '); + } + + const token = query.token; + const x = Number(query.x); + const y = Number(query.y); + const top = Number(query.top); + const bottom = Number(query.bottom); + const left = Number(query.left); + const right = Number(query.right); + const name = query.name; + if (typeof name !== 'string') { + throw new Error('Expecting name'); + } + if (name === '') { + throw new Error('No empty name'); + } + let characterLayers = query.characterLayers; + if (characterLayers === null) { + throw new Error('Expecting skin'); + } + if (typeof characterLayers === 'string') { + characterLayers = [ characterLayers ]; + } + + const userUuid = await jwtTokenManager.getUserUuidFromToken(token); + + let memberTags: string[] = []; + let memberTextures: CharacterTexture[] = []; + const room = await socketManager.getOrCreateRoom(roomId); + if(room.isFull){ + throw new Error('Room is full'); + } + try { + const userData = await adminApi.fetchMemberDataByUuid(userUuid); + //console.log('USERDATA', userData) + memberTags = userData.tags; + memberTextures = userData.textures; + if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) { + throw new Error('No correct tags') + } + //console.log('access granted for user '+userUuid+' and room '+roomId); + } catch (e) { + console.log('access not granted for user '+userUuid+' and room '+roomId); + throw new Error('Client cannot acces this ressource.') + } + + // Generate characterLayers objects from characterLayers string[] + const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures); + + if (upgradeAborted.aborted) { + console.log("Ouch! Client disconnected before we could upgrade it!"); + /* You must not upgrade now */ + return; + } + + /* This immediately calls open handler, you must not use res after this call */ + res.upgrade({ + // Data passed here is accessible on the "websocket" socket object. + url, + token, + userUuid, + roomId, + name, + characterLayers: characterLayerObjs, + tags: memberTags, + textures: memberTextures, + position: { + x: x, + y: y, + direction: 'down', + moving: false + } as PointInterface, + viewport: { + top, + right, + bottom, + left + } + }, + /* Spell these correctly */ + websocketKey, + websocketProtocol, + websocketExtensions, + context); + + } catch (e) { + if (e instanceof Error) { + console.log(e.message); + res.writeStatus("401 Unauthorized").end(e.message); + } else { + console.log(e); + res.writeStatus("500 Internal Server Error").end('An error occurred'); + } + return; + } + })(); + }, + /* Handlers */ + open: (ws) => { + // Let's join the room + const client = this.initClient(ws); //todo: into the upgrade instead? + socketManager.handleJoinRoom(client); + resetPing(client); + + //get data information and shwo messages + adminApi.fetchMemberDataByUuid(client.userUuid).then((res: FetchMemberDataByUuidResponse) => { + if (!res.messages) { + return; + } + res.messages.forEach((c: unknown) => { + const messageToSend = c as { type: string, message: string }; + socketManager.emitSendUserMessage({ + userUuid: client.userUuid, + type: messageToSend.type, + message: messageToSend.message + }) + }); + }).catch((err) => { + console.error('fetchMemberDataByUuid => err', err); + }); + }, + message: (ws, arrayBuffer, isBinary): void => { + const client = ws as ExSocketInterface; + const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer)); + + if (message.hasViewportmessage()) { + socketManager.handleViewport(client, message.getViewportmessage() as ViewportMessage); + } else if (message.hasUsermovesmessage()) { + socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); + } else if (message.hasSetplayerdetailsmessage()) { + socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage); + } else if (message.hasSilentmessage()) { + socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage); + } else if (message.hasItemeventmessage()) { + socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); + } else if (message.hasWebrtcsignaltoservermessage()) { + socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { + socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + } else if (message.hasPlayglobalmessage()) { + socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage); + } else if (message.hasReportplayermessage()){ + socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); + } else if (message.hasQueryjitsijwtmessage()){ + socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); + } + + /* Ok is false if backpressure was built up, wait for drain */ + //let ok = ws.send(message, isBinary); + }, + drain: (ws) => { + console.log('WebSocket backpressure: ' + ws.getBufferedAmount()); + }, + close: (ws, code, message) => { + const Client = (ws as ExSocketInterface); + try { + Client.disconnecting = true; + //leave room + socketManager.leaveRoom(Client); + } catch (e) { + console.error('An error occurred on "disconnect"'); + console.error(e); + } + } + }) + } + + //eslint-disable-next-line @typescript-eslint/no-explicit-any + private initClient(ws: any): ExSocketInterface { + const client : ExSocketInterface = ws; + client.userId = this.nextUserId; + this.nextUserId++; + client.userUuid = ws.userUuid; + client.token = ws.token; + client.batchedMessages = new BatchMessage(); + client.batchTimeout = null; + client.emitInBatch = (payload: SubMessage): void => { + emitInBatch(client, payload); + } + client.disconnecting = false; + + client.name = ws.name; + client.tags = ws.tags; + client.textures = ws.textures; + client.characterLayers = ws.characterLayers; + client.roomId = ws.roomId; + return client; + } +} diff --git a/back/src/Controller/MapController.ts b/back/src/Controller/MapController.ts new file mode 100644 index 00000000..abe34886 --- /dev/null +++ b/back/src/Controller/MapController.ts @@ -0,0 +1,70 @@ +import {OK} from "http-status-codes"; +import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; +import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; +import {BaseController} from "./BaseController"; +import {parse} from "query-string"; +import {adminApi} from "../Services/AdminApi"; + +//todo: delete this +export class MapController extends BaseController{ + + constructor(private App : TemplatedApp) { + super(); + this.App = App; + this.getMapUrl(); + } + + + // Returns a map mapping map name to file name of the map + getMapUrl() { + this.App.options("/map", (res: HttpResponse, req: HttpRequest) => { + this.addCorsHeaders(res); + + res.end(); + }); + + this.App.get("/map", (res: HttpResponse, req: HttpRequest) => { + + res.onAborted(() => { + console.warn('/map request was aborted'); + }) + + const query = parse(req.getQuery()); + + if (typeof query.organizationSlug !== 'string') { + console.error('Expected organizationSlug parameter'); + res.writeStatus("400 Bad request"); + this.addCorsHeaders(res); + res.end("Expected organizationSlug parameter"); + } + if (typeof query.worldSlug !== 'string') { + console.error('Expected worldSlug parameter'); + res.writeStatus("400 Bad request"); + this.addCorsHeaders(res); + res.end("Expected worldSlug parameter"); + } + if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { + console.error('Expected only one roomSlug parameter'); + res.writeStatus("400 Bad request"); + this.addCorsHeaders(res); + res.end("Expected only one roomSlug parameter"); + } + + (async () => { + try { + const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined); + + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end(JSON.stringify(mapDetails)); + } catch (e) { + console.error(e.message || e); + res.writeStatus("500 Internal Server Error") + this.addCorsHeaders(res); + res.end("An error occurred"); + } + })(); + + }); + } +} diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts index fc314059..e854cf43 100644 --- a/back/src/Controller/PrometheusController.ts +++ b/back/src/Controller/PrometheusController.ts @@ -1,23 +1,20 @@ -import { App } from "../Server/sifrr.server"; -import { HttpRequest, HttpResponse } from "uWebSockets.js"; -import { register, collectDefaultMetrics } from "prom-client"; +import {App} from "../Server/sifrr.server"; +import {HttpRequest, HttpResponse} from "uWebSockets.js"; +const register = require('prom-client').register; +const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; export class PrometheusController { constructor(private App: App) { collectDefaultMetrics({ + timeout: 10000, gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets. }); this.App.get("/metrics", this.metrics.bind(this)); - this.App.get("/metrics.json", this.metricsAsJSON.bind(this)); } private metrics(res: HttpResponse, req: HttpRequest): void { - res.writeHeader("Content-Type", register.contentType); + res.writeHeader('Content-Type', register.contentType); res.end(register.metrics()); } - private metricsAsJSON(res: HttpResponse, req: HttpRequest): void { - res.writeHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(register.getMetricsAsJSON())); - } } diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index f0f46a62..56c49284 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -1,31 +1,27 @@ +const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY"; +const URL_ROOM_STARTED = "/Floor0/floor0.json"; const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; -const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; -const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || ""; +const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; +const ADMIN_API_URL = process.env.ADMIN_API_URL || 'http://admin'; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; +const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; -const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; -const JITSI_ISS = process.env.JITSI_ISS || ""; -const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; -const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080; -const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051; -export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ""; -export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); -export const REDIS_HOST = process.env.REDIS_HOST || undefined; -export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379; -export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined; -export const STORE_VARIABLES_FOR_LOCAL_MAPS = process.env.STORE_VARIABLES_FOR_LOCAL_MAPS === "true"; +const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; +const JITSI_ISS = process.env.JITSI_ISS || ''; +const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; export { + SECRET_KEY, + URL_ROOM_STARTED, MINIMUM_DISTANCE, ADMIN_API_URL, ADMIN_API_TOKEN, - HTTP_PORT, - GRPC_PORT, + MAX_USERS_PER_ROOM, GROUP_RADIUS, ALLOW_ARTILLERY, CPU_OVERHEAT_THRESHOLD, JITSI_URL, JITSI_ISS, - SECRET_JITSI_KEY, -}; + SECRET_JITSI_KEY +} diff --git a/back/src/Model/Admin.ts b/back/src/Model/Admin.ts deleted file mode 100644 index 93396fa8..00000000 --- a/back/src/Model/Admin.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - ServerToAdminClientMessage, - UserJoinedRoomMessage, - UserLeftRoomMessage, -} from "../Messages/generated/messages_pb"; -import { AdminSocket } from "../RoomManager"; - -export class Admin { - public constructor(private readonly socket: AdminSocket) {} - - public sendUserJoin(uuid: string, name: string, ip: string): void { - const serverToAdminClientMessage = new ServerToAdminClientMessage(); - - const userJoinedRoomMessage = new UserJoinedRoomMessage(); - userJoinedRoomMessage.setUuid(uuid); - userJoinedRoomMessage.setName(name); - userJoinedRoomMessage.setIpaddress(ip); - - serverToAdminClientMessage.setUserjoinedroom(userJoinedRoomMessage); - - this.socket.write(serverToAdminClientMessage); - } - - public sendUserLeft(uuid: string /*, name: string, ip: string*/): void { - const serverToAdminClientMessage = new ServerToAdminClientMessage(); - - const userLeftRoomMessage = new UserLeftRoomMessage(); - userLeftRoomMessage.setUuid(uuid); - - serverToAdminClientMessage.setUserleftroom(userLeftRoomMessage); - - this.socket.write(serverToAdminClientMessage); - } -} diff --git a/back/src/Model/Distance.ts b/back/src/Model/Distance.ts new file mode 100644 index 00000000..150c144f --- /dev/null +++ b/back/src/Model/Distance.ts @@ -0,0 +1,7 @@ +import {MessageUserPosition} from "../Model/Websocket/MessageUserPosition"; + +export interface Distance { + distance: number, + first: MessageUserPosition, + second: MessageUserPosition, +} \ No newline at end of file diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 7d7b24a5..eaad701a 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -1,226 +1,141 @@ -import { PointInterface } from "./Websocket/PointInterface"; -import { Group } from "./Group"; -import { User, UserSocket } from "./User"; -import { PositionInterface } from "_Model/PositionInterface"; -import { - EmoteCallback, - EntersCallback, - LeavesCallback, - MovesCallback, - PlayerDetailsUpdatedCallback, -} from "_Model/Zone"; -import { PositionNotifier } from "./PositionNotifier"; -import { Movable } from "_Model/Movable"; -import { - BatchToPusherMessage, - BatchToPusherRoomMessage, - EmoteEventMessage, - ErrorMessage, - JoinRoomMessage, - SetPlayerDetailsMessage, - SubToPusherRoomMessage, - VariableMessage, - VariableWithTagMessage, - ServerToClientMessage, -} from "../Messages/generated/messages_pb"; -import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; -import { RoomSocket, ZoneSocket } from "src/RoomManager"; -import { Admin } from "../Model/Admin"; -import { adminApi } from "../Services/AdminApi"; -import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData"; -import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist"; -import { mapFetcher } from "../Services/MapFetcher"; -import { VariablesManager } from "../Services/VariablesManager"; -import { ADMIN_API_URL } from "../Enum/EnvironmentVariable"; -import { LocalUrlError } from "../Services/LocalUrlError"; -import { emitErrorOnRoomSocket } from "../Services/MessageHelpers"; -import { VariableError } from "../Services/VariableError"; -import { isRoomRedirect } from "../Services/AdminApi/RoomRedirect"; +import {PointInterface} from "./Websocket/PointInterface"; +import {Group} from "./Group"; +import {User} from "./User"; +import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import {PositionInterface} from "_Model/PositionInterface"; +import {Identificable} from "_Model/Websocket/Identificable"; +import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; +import {PositionNotifier} from "./PositionNotifier"; +import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import {Movable} from "_Model/Movable"; +import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; +import {arrayIntersect} from "../Services/ArrayHelper"; +import {MAX_USERS_PER_ROOM} from "../Enum/EnvironmentVariable"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; -export class GameRoom { - // Users, sorted by ID - private readonly users = new Map(); - private readonly usersByUuid = new Map(); - private readonly groups = new Set(); - private readonly admins = new Set(); +export enum GameRoomPolicyTypes { + ANONYMUS_POLICY = 1, + MEMBERS_ONLY_POLICY, + USE_TAGS_POLICY, +} - private itemsState = new Map(); +export class GameRoom { + private readonly minDistance: number; + private readonly groupRadius: number; + + // Users, sorted by ID + private readonly users: Map; + private readonly groups: Set; + + private readonly connectCallback: ConnectCallback; + private readonly disconnectCallback: DisconnectCallback; + + private itemsState: Map = new Map(); private readonly positionNotifier: PositionNotifier; - private versionNumber: number = 1; - private nextUserId: number = 1; + public readonly roomId: string; + public readonly anonymous: boolean; + public tags: string[]; + public policyType: GameRoomPolicyTypes; + public readonly roomSlug: string; + public readonly worldSlug: string = ''; + public readonly organizationSlug: string = ''; - private roomListeners: Set = new Set(); + constructor(roomId: string, + connectCallback: ConnectCallback, + disconnectCallback: DisconnectCallback, + minDistance: number, + groupRadius: number, + onEnters: EntersCallback, + onMoves: MovesCallback, + onLeaves: LeavesCallback) + { + this.roomId = roomId; + this.anonymous = isRoomAnonymous(roomId); + this.tags = []; + this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; - private constructor( - public readonly roomUrl: string, - private mapUrl: string, - private readonly connectCallback: ConnectCallback, - private readonly disconnectCallback: DisconnectCallback, - private readonly minDistance: number, - private readonly groupRadius: number, - onEnters: EntersCallback, - onMoves: MovesCallback, - onLeaves: LeavesCallback, - onEmote: EmoteCallback, - onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback - ) { + if (this.anonymous) { + this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); + } else { + const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + this.roomSlug = roomSlug; + this.organizationSlug = organizationSlug; + this.worldSlug = worldSlug; + } + + + this.users = new Map(); + this.groups = new Set(); + this.connectCallback = connectCallback; + this.disconnectCallback = disconnectCallback; + this.minDistance = minDistance; + this.groupRadius = groupRadius; // A zone is 10 sprites wide. - this.positionNotifier = new PositionNotifier( - 320, - 320, - onEnters, - onMoves, - onLeaves, - onEmote, - onPlayerDetailsUpdated - ); + this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves); } - public static async create( - roomUrl: string, - connectCallback: ConnectCallback, - disconnectCallback: DisconnectCallback, - minDistance: number, - groupRadius: number, - onEnters: EntersCallback, - onMoves: MovesCallback, - onLeaves: LeavesCallback, - onEmote: EmoteCallback, - onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback - ): Promise { - const mapDetails = await GameRoom.getMapDetails(roomUrl); - - const gameRoom = new GameRoom( - roomUrl, - mapDetails.mapUrl, - connectCallback, - disconnectCallback, - minDistance, - groupRadius, - onEnters, - onMoves, - onLeaves, - onEmote, - onPlayerDetailsUpdated - ); - - return gameRoom; + public getGroups(): Group[] { + return Array.from(this.groups.values()); } public getUsers(): Map { return this.users; } - public getUserByUuid(uuid: string): User | undefined { - return this.usersByUuid.get(uuid); - } - public getUserById(id: number): User | undefined { - return this.users.get(id); - } - public getUsersByUuid(uuid: string): User[] { - const userList: User[] = []; - for (const user of this.users.values()) { - if (user.uuid === uuid) { - userList.push(user); - } - } - return userList; - } - - public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User { - const positionMessage = joinRoomMessage.getPositionmessage(); - if (positionMessage === undefined) { - throw new Error("Missing position message"); - } - const position = ProtobufUtils.toPointInterface(positionMessage); - - const user = new User( - this.nextUserId, - joinRoomMessage.getUseruuid(), - joinRoomMessage.getIpaddress(), - position, - false, - this.positionNotifier, - socket, - joinRoomMessage.getTagList(), - joinRoomMessage.getVisitcardurl(), - joinRoomMessage.getName(), - ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()), - joinRoomMessage.getCompanion() - ); - this.nextUserId++; - this.users.set(user.id, user); - this.usersByUuid.set(user.uuid, user); + public join(socket : ExSocketInterface, userPosition: PointInterface): void { + const user = new User(socket.userId, socket.userUuid, userPosition, false, this.positionNotifier, socket); + this.users.set(socket.userId, user); + // Let's call update position to trigger the join / leave room + //this.updatePosition(socket, userPosition); this.updateUserGroup(user); - - // Notify admins - for (const admin of this.admins) { - admin.sendUserJoin(user.uuid, user.name, user.IPAddress); - } - - return user; } - public leave(user: User) { - const userObj = this.users.get(user.id); + public leave(user : Identificable){ + const userObj = this.users.get(user.userId); if (userObj === undefined) { - console.warn("User ", user.id, "does not belong to this game room! It should!"); + console.warn('User ', user.userId, 'does not belong to world! It should!'); } - if (userObj !== undefined && typeof userObj.group !== "undefined") { + if (userObj !== undefined && typeof userObj.group !== 'undefined') { this.leaveGroup(userObj); } - - if (user.hasFollowers()) { - user.stopLeading(); - } - if (user.following) { - user.following.delFollower(user); - } - - this.users.delete(user.id); - this.usersByUuid.delete(user.uuid); + this.users.delete(user.userId); if (userObj !== undefined) { + this.positionNotifier.removeViewport(userObj); this.positionNotifier.leave(userObj); } + } - // Notify admins - for (const admin of this.admins) { - admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/); - } + get isFull(): boolean { + return this.users.size >= MAX_USERS_PER_ROOM; } public isEmpty(): boolean { - return this.users.size === 0 && this.admins.size === 0; + return this.users.size === 0; } - public updatePosition(user: User, userPosition: PointInterface): void { + public updatePosition(socket : Identificable, userPosition: PointInterface): void { + const user = this.users.get(socket.userId); + if(typeof user === 'undefined') { + return; + } + user.setPosition(userPosition); this.updateUserGroup(user); } - updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) { - if (playerDetailsMessage.getRemoveoutlinecolor()) { - user.outlineColor = undefined; - } else { - user.outlineColor = playerDetailsMessage.getOutlinecolor(); - } - } - private updateUserGroup(user: User): void { + user.group?.updatePosition(); + if (user.silent) { return; } - const group = user.group; - const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user); - - if (group === undefined) { + if (user.group === undefined) { // If the user is not part of a group: // should he join a group? @@ -229,125 +144,38 @@ export class GameRoom { return; } + const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user); + if (closestItem !== null) { if (closestItem instanceof Group) { // Let's join the group! closestItem.join(user); - closestItem.setOutOfBounds(false); } else { - const closestUser: User = closestItem; - const group: Group = new Group( - this.roomUrl, - [user, closestUser], - this.groupRadius, - this.connectCallback, - this.disconnectCallback, - this.positionNotifier - ); + const closestUser : User = closestItem; + const group: Group = new Group(this.roomId,[ + user, + closestUser + ], this.connectCallback, this.disconnectCallback, this.positionNotifier); this.groups.add(group); } } + } else { - let hasKickOutSomeone = false; - let followingMembers: User[] = []; - - const previewNewGroupPosition = group.previewGroupPosition(); - - if (!previewNewGroupPosition) { + // If the user is part of a group: + // should he leave the group? + const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition()); + if (distance > this.groupRadius) { this.leaveGroup(user); - return; - } - - if (user.hasFollowers() || user.following) { - followingMembers = user.hasFollowers() - ? group.getUsers().filter((currentUser) => currentUser.following === user) - : group.getUsers().filter((currentUser) => currentUser.following === user.following); - - // If all group members are part of the same follow group - if (group.getUsers().length - 1 === followingMembers.length) { - let isOutOfBounds = false; - - // If a follower is far away from the leader, "outOfBounds" is set to true - for (const member of followingMembers) { - const distance = GameRoom.computeDistanceBetweenPositions( - member.getPosition(), - previewNewGroupPosition - ); - - if (distance > this.groupRadius) { - isOutOfBounds = true; - break; - } - } - group.setOutOfBounds(isOutOfBounds); - } - } - - // Check if the moving user has kicked out another user - for (const headMember of group.getGroupHeads()) { - if (!headMember.group) { - this.leaveGroup(headMember); - continue; - } - - const headPosition = headMember.getPosition(); - const distance = GameRoom.computeDistanceBetweenPositions(headPosition, previewNewGroupPosition); - - if (distance > this.groupRadius) { - hasKickOutSomeone = true; - break; - } - } - - /** - * If the current moving user has kicked another user from the radius, - * the moving user leaves the group because he is too far away. - */ - const userDistance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), previewNewGroupPosition); - - if (hasKickOutSomeone && userDistance > this.groupRadius) { - if (user.hasFollowers() && group.getUsers().length === 3 && followingMembers.length === 1) { - const other = group - .getUsers() - .find((currentUser) => !currentUser.hasFollowers() && !currentUser.following); - if (other) { - this.leaveGroup(other); - } - } else if (user.hasFollowers()) { - this.leaveGroup(user); - for (const member of followingMembers) { - this.leaveGroup(member); - } - - // Re-create a group with the followers - const newGroup: Group = new Group( - this.roomUrl, - [user, ...followingMembers], - this.groupRadius, - this.connectCallback, - this.disconnectCallback, - this.positionNotifier - ); - this.groups.add(newGroup); - } else { - this.leaveGroup(user); - } } } - - user.group?.updatePosition(); - user.group?.searchForNearbyUsers(); } - public sendToOthersInGroupIncludingUser(user: User, message: ServerToClientMessage): void { - user.group?.getUsers().forEach((currentUser: User) => { - if (currentUser.id !== user.id) { - currentUser.socket.write(message); - } - }); - } - - setSilent(user: User, silent: boolean) { + setSilent(socket: Identificable, silent: boolean) { + const user = this.users.get(socket.userId); + if(typeof user === 'undefined') { + console.warn('In setSilent, could not find user with ID "'+socket.userId+'" in world.'); + return; + } if (user.silent === silent) { return; } @@ -358,7 +186,7 @@ export class GameRoom { } if (!silent) { // If we are back to life, let's trigger a position update to see if we can join some group. - this.updatePosition(user, user.getPosition()); + this.updatePosition(socket, user.getPosition()); } } @@ -374,9 +202,10 @@ export class GameRoom { } group.leave(user); if (group.isEmpty()) { + this.positionNotifier.leave(group); group.destroy(); if (!this.groups.has(group)) { - throw new Error(`Could not find group ${group.getId()} referenced by user ${user.id} in World.`); + throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World."); } this.groups.delete(group); //todo: is the group garbage collected? @@ -394,15 +223,16 @@ export class GameRoom { * OR * - close enough to a group (distance <= groupRadius) */ - private searchClosestAvailableUserOrGroup(user: User): User | Group | null { + private searchClosestAvailableUserOrGroup(user: User): User|Group|null + { let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius); let matchingItem: User | Group | null = null; this.users.forEach((currentUser, userId) => { // Let's only check users that are not part of a group - if (typeof currentUser.group !== "undefined") { + if (typeof currentUser.group !== 'undefined') { return; } - if (currentUser === user) { + if(currentUser === user) { return; } if (currentUser.silent) { @@ -411,7 +241,7 @@ export class GameRoom { const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. - if (distance <= minimumDistanceFound && distance <= this.minDistance) { + if(distance <= minimumDistanceFound && distance <= this.minDistance) { minimumDistanceFound = distance; matchingItem = currentUser; } @@ -422,7 +252,7 @@ export class GameRoom { return; } const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); - if (distance <= minimumDistanceFound && distance <= this.groupRadius) { + if(distance <= minimumDistanceFound && distance <= this.groupRadius) { minimumDistanceFound = distance; matchingItem = group; } @@ -431,15 +261,15 @@ export class GameRoom { return matchingItem; } - public static computeDistance(user1: User, user2: User): number { + public static computeDistance(user1: User, user2: User): number + { const user1Position = user1.getPosition(); const user2Position = user2.getPosition(); - return Math.sqrt( - Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2) - ); + return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)); } - public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number { + public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number + { return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); } @@ -451,207 +281,17 @@ export class GameRoom { return this.itemsState; } - public async setVariable(name: string, value: string, user: User): Promise { - // First, let's check if "user" is allowed to modify the variable. - const variableManager = await this.getVariableManager(); - try { - const readableBy = variableManager.setVariable(name, value, user); - - // If the variable was not changed, let's not dispatch anything. - if (readableBy === false) { - return; - } - - // TODO: should we batch those every 100ms? - const variableMessage = new VariableWithTagMessage(); - variableMessage.setName(name); - variableMessage.setValue(value); - if (readableBy) { - variableMessage.setReadableby(readableBy); - } - - const subMessage = new SubToPusherRoomMessage(); - subMessage.setVariablemessage(variableMessage); - - const batchMessage = new BatchToPusherRoomMessage(); - batchMessage.addPayload(subMessage); - - // Dispatch the message on the room listeners - for (const socket of this.roomListeners) { - socket.write(batchMessage); - } - } catch (e) { - if (e instanceof VariableError) { - // Ok, we have an error setting a variable. Either the user is trying to hack the map... or the map - // is not up to date. So let's try to reload the map from scratch. - if (this.variableManagerLastLoad === undefined) { - throw e; - } - const lastLoaded = new Date().getTime() - this.variableManagerLastLoad.getTime(); - if (lastLoaded < 10000) { - console.log( - 'An error occurred while setting the "' + - name + - "\" variable. But we tried to reload the map less than 10 seconds ago, so let's fail." - ); - // Do not try to reload if we tried to reload less than 10 seconds ago. - throw e; - } - - // Reset the variable manager - this.variableManagerPromise = undefined; - this.mapPromise = undefined; - - console.log( - 'An error occurred while setting the "' + name + "\" variable. Let's reload the map and try again" - ); - // Try to set the variable again! - await this.setVariable(name, value, user); - } else { - throw e; - } + setViewport(socket : Identificable, viewport: ViewportInterface): Movable[] { + const user = this.users.get(socket.userId); + if(typeof user === 'undefined') { + console.warn('In setViewport, could not find user with ID "'+socket.userId+'" in world.'); + return []; } + return this.positionNotifier.setViewport(user, viewport); } - public addZoneListener(call: ZoneSocket, x: number, y: number): Set { - return this.positionNotifier.addZoneListener(call, x, y); - } - - public removeZoneListener(call: ZoneSocket, x: number, y: number): void { - return this.positionNotifier.removeZoneListener(call, x, y); - } - - public adminJoin(admin: Admin): void { - this.admins.add(admin); - - // Let's send all connected users - for (const user of this.users.values()) { - admin.sendUserJoin(user.uuid, user.name, user.IPAddress); - } - } - - public adminLeave(admin: Admin): void { - this.admins.delete(admin); - } - - public incrementVersion(): number { - this.versionNumber++; - return this.versionNumber; - } - - public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) { - this.positionNotifier.emitEmoteEvent(user, emoteEventMessage); - } - - public addRoomListener(socket: RoomSocket) { - this.roomListeners.add(socket); - } - - public removeRoomListener(socket: RoomSocket) { - this.roomListeners.delete(socket); - } - - /** - * Connects to the admin server to fetch map details. - * If there is no admin server, the map details are generated by analysing the map URL (that must be in the form: /_/instance/map_url) - */ - private static async getMapDetails(roomUrl: string): Promise { - if (!ADMIN_API_URL) { - const roomUrlObj = new URL(roomUrl); - - const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname); - if (!match) { - console.error("Unexpected room URL", roomUrl); - throw new Error('Unexpected room URL "' + roomUrl + '"'); - } - - const mapUrl = roomUrlObj.protocol + "//" + match[1]; - - return { - mapUrl, - policy_type: 1, - textures: [], - tags: [], - }; - } - - const result = await adminApi.fetchMapDetails(roomUrl); - if (isRoomRedirect(result)) { - console.error("Unexpected room redirect received while querying map details", result); - throw new Error("Unexpected room redirect received while querying map details"); - } - return result; - } - - private mapPromise: Promise | undefined; - - /** - * Returns a promise to the map file. - * @throws LocalUrlError if the map we are trying to load is hosted on a local network - * @throws Error - */ - private getMap(): Promise { - if (!this.mapPromise) { - this.mapPromise = mapFetcher.fetchMap(this.mapUrl); - } - - return this.mapPromise; - } - - private variableManagerPromise: Promise | undefined; - private variableManagerLastLoad: Date | undefined; - - private getVariableManager(): Promise { - if (!this.variableManagerPromise) { - this.variableManagerLastLoad = new Date(); - this.variableManagerPromise = this.getMap() - .then((map) => { - const variablesManager = new VariablesManager(this.roomUrl, map); - return variablesManager.init(); - }) - .catch((e) => { - if (e instanceof LocalUrlError) { - // If we are trying to load a local URL, we are probably in test mode. - // In this case, let's bypass the server-side checks completely. - - // Note: we run this message inside a setTimeout so that the room listeners can have time to connect. - setTimeout(() => { - for (const roomListener of this.roomListeners) { - emitErrorOnRoomSocket( - roomListener, - "You are loading a local map. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled." - ); - } - }, 1000); - - const variablesManager = new VariablesManager(this.roomUrl, null); - return variablesManager.init(); - } else { - // An error occurred while loading the map - // Right now, let's bypass the error. In the future, we should make sure the user is aware of that - // and that he/she will act on it to fix the problem. - - // Note: we run this message inside a setTimeout so that the room listeners can have time to connect. - setTimeout(() => { - for (const roomListener of this.roomListeners) { - emitErrorOnRoomSocket( - roomListener, - "Your map does not seem accessible from the WorkAdventure servers. Is it behind a firewall or a proxy? Your map should be accessible from the WorkAdventure servers. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled." - ); - } - }, 1000); - - const variablesManager = new VariablesManager(this.roomUrl, null); - return variablesManager.init(); - } - }); - } - return this.variableManagerPromise; - } - - public async getVariablesForTags(tags: string[]): Promise> { - const variablesManager = await this.getVariableManager(); - return variablesManager.getVariablesForTags(tags); + canAccess(userTags: string[]): boolean { + return arrayIntersect(userTags, this.tags); } } diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index c14d509f..f26a0e0d 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,38 +1,36 @@ -import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom"; +import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { User } from "./User"; -import { PositionInterface } from "_Model/PositionInterface"; -import { Movable } from "_Model/Movable"; -import { PositionNotifier } from "_Model/PositionNotifier"; -import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable"; -import type { Zone } from "../Model/Zone"; +import {PositionInterface} from "_Model/PositionInterface"; +import {Movable} from "_Model/Movable"; +import {PositionNotifier} from "_Model/PositionNotifier"; +import {gaugeManager} from "../Services/GaugeManager"; export class Group implements Movable { + static readonly MAX_PER_GROUP = 4; + private static nextId: number = 1; private id: number; private users: Set; private x!: number; private y!: number; + private hasEditedGauge: boolean = false; private wasDestroyed: boolean = false; private roomId: string; - private currentZone: Zone | null = null; - /** - * When outOfBounds = true, a user if out of the bounds of the group BUT still considered inside it (because we are in following mode) - */ - private outOfBounds = false; - constructor( - roomId: string, - users: User[], - private groupRadius: number, - private connectCallback: ConnectCallback, - private disconnectCallback: DisconnectCallback, - private positionNotifier: PositionNotifier - ) { + + constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { this.roomId = roomId; this.users = new Set(); this.id = Group.nextId; Group.nextId++; + //we only send a event for prometheus metrics if the group lives more than 5 seconds + setTimeout(() => { + if (!this.wasDestroyed) { + this.hasEditedGauge = true; + gaugeManager.incNbGroupsPerRoomGauge(roomId); + } + }, 5000); users.forEach((user: User) => { this.join(user); @@ -45,7 +43,7 @@ export class Group implements Movable { return Array.from(this.users.values()); } - getId(): number { + getId() : number { return this.id; } @@ -55,43 +53,10 @@ export class Group implements Movable { getPosition(): PositionInterface { return { x: this.x, - y: this.y, + y: this.y }; } - /** - * Returns the list of users of the group, ignoring any "followers". - * Useful to compute the position of the group if a follower is "trapped" far away from the the leader. - */ - getGroupHeads(): User[] { - return Array.from(this.users).filter((user) => user.group?.leader === user || !user.following); - } - - /** - * Preview the position of the group but don't update it - */ - previewGroupPosition(): { x: number; y: number } | undefined { - const users = this.getGroupHeads(); - - let x = 0; - let y = 0; - - if (users.length === 0) { - return undefined; - } - - users.forEach((user: User) => { - const position = user.getPosition(); - x += position.x; - y += position.y; - }); - - x /= users.length; - y /= users.length; - - return { x, y }; - } - /** * Computes the barycenter of all users (i.e. the center of the group) */ @@ -99,63 +64,50 @@ export class Group implements Movable { const oldX = this.x; const oldY = this.y; + let x = 0; + let y = 0; // Let's compute the barycenter of all users. - const newPosition = this.previewGroupPosition(); - - if (!newPosition) { - return; + this.users.forEach((user: User) => { + const position = user.getPosition(); + x += position.x; + y += position.y; + }); + x /= this.users.size; + y /= this.users.size; + if (this.users.size === 0) { + throw new Error("EMPTY GROUP FOUND!!!"); } - - const { x, y } = newPosition; - this.x = x; this.y = y; - if (this.outOfBounds) { - return; - } - if (oldX === undefined) { - this.currentZone = this.positionNotifier.enter(this); + this.positionNotifier.enter(this); } else { - this.currentZone = this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY }); - } - } - - searchForNearbyUsers(): void { - if (!this.currentZone) return; - - for (const user of this.positionNotifier.getAllUsersInSquareAroundZone(this.currentZone)) { - // Todo: Merge two groups with a leader - if (user.group || this.isFull()) return; //we ignore users that are already in a group. - const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), this.getPosition()); - if (distance < this.groupRadius) { - this.join(user); - this.setOutOfBounds(false); - this.updatePosition(); - } + this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY}); } } isFull(): boolean { - return this.users.size >= MAX_PER_GROUP; + return this.users.size >= Group.MAX_PER_GROUP; } isEmpty(): boolean { return this.users.size <= 1; } - join(user: User): void { + join(user: User): void + { // Broadcast on the right event this.connectCallback(user, this); this.users.add(user); user.group = this; } - leave(user: User): void { + leave(user: User): void + { const success = this.users.delete(user); if (success === false) { - throw new Error(`Could not find user ${user.id} in the group ${this.id}`); + throw new Error("Could not find user "+user.id+" in the group "+this.id); } user.group = undefined; @@ -171,44 +123,16 @@ export class Group implements Movable { * Let's kick everybody out. * Usually used when there is only one user left. */ - destroy(): void { - if (!this.outOfBounds) { - this.positionNotifier.leave(this); - } - + destroy(): void + { + if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId); for (const user of this.users) { this.leave(user); } this.wasDestroyed = true; } - get getSize() { + get getSize(){ return this.users.size; } - - /** - * A group can have at most one person leading the way in it. - */ - get leader(): User | undefined { - for (const user of this.users) { - if (user.hasFollowers()) { - return user; - } - } - return undefined; - } - - setOutOfBounds(outOfBounds: boolean): void { - if (this.outOfBounds === true && outOfBounds === false) { - this.positionNotifier.enter(this); - this.outOfBounds = false; - } else if (this.outOfBounds === false && outOfBounds === true) { - this.positionNotifier.leave(this); - this.outOfBounds = true; - } - } - - get getOutOfBounds() { - return this.outOfBounds; - } } diff --git a/back/src/Model/Movable.ts b/back/src/Model/Movable.ts index ca586b7c..173db0ae 100644 --- a/back/src/Model/Movable.ts +++ b/back/src/Model/Movable.ts @@ -1,8 +1,8 @@ -import { PositionInterface } from "_Model/PositionInterface"; +import {PositionInterface} from "_Model/PositionInterface"; /** * A physical object that can be placed into a Zone */ export interface Movable { - getPosition(): PositionInterface; + getPosition(): PositionInterface } diff --git a/back/src/Model/PositionInterface.ts b/back/src/Model/PositionInterface.ts index 65636759..d3b0dd47 100644 --- a/back/src/Model/PositionInterface.ts +++ b/back/src/Model/PositionInterface.ts @@ -1,4 +1,4 @@ export interface PositionInterface { - x: number; - y: number; + x: number, + y: number } diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index b059999a..215d6ee6 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -8,67 +8,80 @@ * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * number of players around the current player. */ -import { - EmoteCallback, - EntersCallback, - LeavesCallback, - MovesCallback, - PlayerDetailsUpdatedCallback, - Zone, -} from "./Zone"; -import { Movable } from "_Model/Movable"; -import { PositionInterface } from "_Model/PositionInterface"; -import { ZoneSocket } from "../RoomManager"; -import { User } from "../Model/User"; -import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb"; +import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone"; +import {PointInterface} from "_Model/Websocket/PointInterface"; +import {User} from "_Model/User"; +import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import {Movable} from "_Model/Movable"; +import {PositionInterface} from "_Model/PositionInterface"; interface ZoneDescriptor { i: number; j: number; } -export function* getNearbyDescriptorsMatrix(middleZoneDescriptor: ZoneDescriptor): Generator { - for (let n = 0; n < 9; n++) { - const i = middleZoneDescriptor.i + ((n % 3) - 1); - const j = middleZoneDescriptor.j + (Math.floor(n / 3) - 1); - - if (i >= 0 && j >= 0) { - yield { i, j }; - } - } -} - export class PositionNotifier { - // TODO: we need a way to clean the zones if no one is in the zone and no one listening (to free memory!) + + // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) private zones: Zone[][] = []; - constructor( - private zoneWidth: number, - private zoneHeight: number, - private onUserEnters: EntersCallback, - private onUserMoves: MovesCallback, - private onUserLeaves: LeavesCallback, - private onEmote: EmoteCallback, - private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback - ) {} + constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback) { + } private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), - }; + } } - public enter(thing: Movable): Zone { + /** + * Sets the viewport coordinates. + * Returns the list of new users to add + */ + public setViewport(user: User, viewport: ViewportInterface): Movable[] { + if (viewport.left > viewport.right || viewport.top > viewport.bottom) { + console.warn('Invalid viewport received: ', viewport); + return []; + } + + const oldZones = user.listenedZones; + const newZones = new Set(); + + const topLeftDesc = this.getZoneDescriptorFromCoordinates(viewport.left, viewport.top); + const bottomRightDesc = this.getZoneDescriptorFromCoordinates(viewport.right, viewport.bottom); + + for (let j = topLeftDesc.j; j <= bottomRightDesc.j; j++) { + for (let i = topLeftDesc.i; i <= bottomRightDesc.i; i++) { + newZones.add(this.getZone(i, j)); + } + } + + const addedZones = [...newZones].filter(x => !oldZones.has(x)); + const removedZones = [...oldZones].filter(x => !newZones.has(x)); + + + let things: Movable[] = []; + for (const zone of addedZones) { + zone.startListening(user); + things = things.concat(Array.from(zone.getThings())) + } + for (const zone of removedZones) { + zone.stopListening(user); + } + + return things; + } + + public enter(thing: Movable): void { const position = thing.getPosition(); const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y); const zone = this.getZone(zoneDesc.i, zoneDesc.j); zone.enter(thing, null, position); - return zone; } - public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): Zone { + public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): void { // Did we change zone? const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y); const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y); @@ -82,11 +95,9 @@ export class PositionNotifier { // Enter new zone newZone.enter(thing, oldZone, newPosition); - return newZone; } else { const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j); zone.move(thing, newPosition); - return zone; } } @@ -97,6 +108,13 @@ export class PositionNotifier { oldZone.leave(thing, null); } + public removeViewport(user: User): void { + // Also, let's stop listening on viewports + for (const zone of user.listenedZones) { + zone.stopListening(user); + } + } + private getZone(i: number, j: number): Zone { let zoneRow = this.zones[j]; if (zoneRow === undefined) { @@ -106,53 +124,9 @@ export class PositionNotifier { let zone = this.zones[j][i]; if (zone === undefined) { - zone = new Zone( - this.onUserEnters, - this.onUserMoves, - this.onUserLeaves, - this.onEmote, - this.onPlayerDetailsUpdated, - i, - j - ); + zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, i, j); this.zones[j][i] = zone; } return zone; } - - public addZoneListener(call: ZoneSocket, x: number, y: number): Set { - const zone = this.getZone(x, y); - zone.addListener(call); - return zone.getThings(); - } - - public removeZoneListener(call: ZoneSocket, x: number, y: number): void { - const zone = this.getZone(x, y); - zone.removeListener(call); - } - - public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) { - const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); - const zone = this.getZone(zoneDesc.i, zoneDesc.j); - zone.emitEmoteEvent(emoteEventMessage); - } - - public *getAllUsersInSquareAroundZone(zone: Zone): Generator { - const zoneDescriptor = this.getZoneDescriptorFromCoordinates(zone.x, zone.y); - for (const d of getNearbyDescriptorsMatrix(zoneDescriptor)) { - const zone = this.getZone(d.i, d.j); - for (const thing of zone.getThings()) { - if (thing instanceof User) { - yield thing; - } - } - } - } - - public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) { - const position = user.getPosition(); - const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y); - const zone = this.getZone(zoneDesc.i, zoneDesc.j); - zone.updatePlayerDetails(user, playerDetails); - } } diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts new file mode 100644 index 00000000..3ac62bca --- /dev/null +++ b/back/src/Model/RoomIdentifier.ts @@ -0,0 +1,30 @@ +//helper functions to parse room IDs + +export const isRoomAnonymous = (roomID: string): boolean => { + if (roomID.startsWith('_/')) { + return true; + } else if(roomID.startsWith('@/')) { + return false; + } else { + throw new Error('Incorrect room ID: '+roomID); + } +} + +export const extractRoomSlugPublicRoomId = (roomId: string): string => { + const idParts = roomId.split('/'); + if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); + return idParts.slice(2).join('/'); +} +export interface extractDataFromPrivateRoomIdResponse { + organizationSlug: string; + worldSlug: string; + roomSlug: string; +} +export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { + const idParts = roomId.split('/'); + if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const organizationSlug = idParts[1]; + const worldSlug = idParts[2]; + const roomSlug = idParts[3]; + return {organizationSlug, worldSlug, roomSlug} +} \ No newline at end of file diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index c6abf52a..86a227f4 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -1,43 +1,22 @@ import { Group } from "./Group"; import { PointInterface } from "./Websocket/PointInterface"; -import { Zone } from "_Model/Zone"; -import { Movable } from "_Model/Movable"; -import { PositionNotifier } from "_Model/PositionNotifier"; -import { ServerDuplexStream } from "grpc"; -import { - BatchMessage, - CompanionMessage, - FollowAbortMessage, - FollowConfirmationMessage, - PusherToBackMessage, - ServerToClientMessage, - SetPlayerDetailsMessage, - SubMessage, -} from "../Messages/generated/messages_pb"; -import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; - -export type UserSocket = ServerDuplexStream; +import {Zone} from "_Model/Zone"; +import {Movable} from "_Model/Movable"; +import {PositionInterface} from "_Model/PositionInterface"; +import {PositionNotifier} from "_Model/PositionNotifier"; +import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; export class User implements Movable { public listenedZones: Set; public group?: Group; - private _following: User | undefined; - private followedBy: Set = new Set(); public constructor( public id: number, - public readonly uuid: string, - public readonly IPAddress: string, + public uuid: string, private position: PointInterface, public silent: boolean, private positionNotifier: PositionNotifier, - public readonly socket: UserSocket, - public readonly tags: string[], - public readonly visitCardUrl: string | null, - public readonly name: string, - public readonly characterLayers: CharacterLayer[], - public readonly companion?: CompanionMessage, - private _outlineColor?: number | undefined + public readonly socket: ExSocketInterface ) { this.listenedZones = new Set(); @@ -53,78 +32,4 @@ export class User implements Movable { this.position = position; this.positionNotifier.updatePosition(this, position, oldPosition); } - - public addFollower(follower: User): void { - this.followedBy.add(follower); - follower._following = this; - - const message = new FollowConfirmationMessage(); - message.setFollower(follower.id); - message.setLeader(this.id); - const clientMessage = new ServerToClientMessage(); - clientMessage.setFollowconfirmationmessage(message); - this.socket.write(clientMessage); - } - - public delFollower(follower: User): void { - this.followedBy.delete(follower); - follower._following = undefined; - - const message = new FollowAbortMessage(); - message.setFollower(follower.id); - message.setLeader(this.id); - const clientMessage = new ServerToClientMessage(); - clientMessage.setFollowabortmessage(message); - this.socket.write(clientMessage); - follower.socket.write(clientMessage); - } - - public hasFollowers(): boolean { - return this.followedBy.size !== 0; - } - - get following(): User | undefined { - return this._following; - } - - public stopLeading(): void { - for (const follower of this.followedBy) { - this.delFollower(follower); - } - } - - private batchedMessages: BatchMessage = new BatchMessage(); - private batchTimeout: NodeJS.Timeout | null = null; - - public emitInBatch(payload: SubMessage): void { - this.batchedMessages.addPayload(payload); - - if (this.batchTimeout === null) { - this.batchTimeout = setTimeout(() => { - /*if (socket.disconnecting) { - return; - }*/ - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setBatchmessage(this.batchedMessages); - - this.socket.write(serverToClientMessage); - this.batchedMessages = new BatchMessage(); - this.batchTimeout = null; - }, 100); - } - } - - public set outlineColor(value: number | undefined) { - this._outlineColor = value; - - const playerDetails = new SetPlayerDetailsMessage(); - if (value === undefined) { - playerDetails.setRemoveoutlinecolor(true); - } else { - playerDetails.setOutlinecolor(value); - } - - this.positionNotifier.updatePlayerDetails(this, playerDetails); - } } diff --git a/back/src/Model/Websocket/CharacterLayer.ts b/back/src/Model/Websocket/CharacterLayer.ts deleted file mode 100644 index 3e428790..00000000 --- a/back/src/Model/Websocket/CharacterLayer.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface CharacterLayer { - name: string; - url: string | undefined; -} diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts new file mode 100644 index 00000000..205032bc --- /dev/null +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -0,0 +1,32 @@ +import {PointInterface} from "./PointInterface"; +import {Identificable} from "./Identificable"; +import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import {BatchMessage, SubMessage} from "../../Messages/generated/messages_pb"; +import {WebSocket} from "uWebSockets.js" +import {CharacterTexture} from "../../Services/AdminApi"; + +export interface CharacterLayer { + name: string, + url: string|undefined +} + +export interface ExSocketInterface extends WebSocket, Identificable { + token: string; + roomId: string; + //userId: number; // A temporary (autoincremented) identifier for this user + userUuid: string; // A unique identifier for this user + name: string; + characterLayers: CharacterLayer[]; + position: PointInterface; + viewport: ViewportInterface; + /** + * Pushes an event that will be sent in the next batch of events + */ + emitInBatch: (payload: SubMessage) => void; + batchedMessages: BatchMessage; + batchTimeout: NodeJS.Timeout|null; + pingTimeout: NodeJS.Timeout|null; + disconnecting: boolean, + tags: string[], + textures: CharacterTexture[], +} diff --git a/back/src/Model/Websocket/GroupUpdateInterface.ts b/back/src/Model/Websocket/GroupUpdateInterface.ts new file mode 100644 index 00000000..34a6d8b1 --- /dev/null +++ b/back/src/Model/Websocket/GroupUpdateInterface.ts @@ -0,0 +1,6 @@ +import {PositionInterface} from "_Model/PositionInterface"; + +export interface GroupUpdateInterface { + position: PositionInterface, + groupId: number, +} diff --git a/back/src/Model/Websocket/ItemEventMessage.ts b/back/src/Model/Websocket/ItemEventMessage.ts index 1bb7f615..b1f9203e 100644 --- a/back/src/Model/Websocket/ItemEventMessage.ts +++ b/back/src/Model/Websocket/ItemEventMessage.ts @@ -1,11 +1,10 @@ import * as tg from "generic-type-guard"; -export const isItemEventMessageInterface = new tg.IsInterface() - .withProperties({ +export const isItemEventMessageInterface = + new tg.IsInterface().withProperties({ itemId: tg.isNumber, event: tg.isString, state: tg.isUnknown, parameters: tg.isUnknown, - }) - .get(); + }).get(); export type ItemEventMessageInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/JoinRoomMessage.ts b/back/src/Model/Websocket/JoinRoomMessage.ts new file mode 100644 index 00000000..2036a441 --- /dev/null +++ b/back/src/Model/Websocket/JoinRoomMessage.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; +import {isPointInterface} from "./PointInterface"; +import {isViewport} from "./ViewportMessage"; + +export const isJoinRoomMessageInterface = + new tg.IsInterface().withProperties({ + roomId: tg.isString, + position: isPointInterface, + viewport: isViewport + }).get(); +export type JoinRoomMessageInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/MessageUserJoined.ts b/back/src/Model/Websocket/MessageUserJoined.ts new file mode 100644 index 00000000..9ae7ab2c --- /dev/null +++ b/back/src/Model/Websocket/MessageUserJoined.ts @@ -0,0 +1,6 @@ +import {PointInterface} from "_Model/Websocket/PointInterface"; + +export class MessageUserJoined { + constructor(public userId: number, public name: string, public characterLayers: string[], public position: PointInterface) { + } +} diff --git a/back/src/Model/Websocket/MessageUserPosition.ts b/back/src/Model/Websocket/MessageUserPosition.ts index 19b57d2e..08035997 100644 --- a/back/src/Model/Websocket/MessageUserPosition.ts +++ b/back/src/Model/Websocket/MessageUserPosition.ts @@ -1,10 +1,11 @@ -import { PointInterface } from "./PointInterface"; +import {PointInterface} from "./PointInterface"; -export class Point implements PointInterface { - constructor( - public x: number, - public y: number, - public direction: string = "none", - public moving: boolean = false - ) {} +export class Point implements PointInterface{ + constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { + } +} + +export class MessageUserPosition { + constructor(public userId: number, public name: string, public characterLayers: string[], public position: PointInterface) { + } } diff --git a/back/src/Model/Websocket/PointInterface.ts b/back/src/Model/Websocket/PointInterface.ts index d7c7826e..afb07a23 100644 --- a/back/src/Model/Websocket/PointInterface.ts +++ b/back/src/Model/Websocket/PointInterface.ts @@ -7,12 +7,11 @@ import * as tg from "generic-type-guard"; readonly moving: boolean; }*/ -export const isPointInterface = new tg.IsInterface() - .withProperties({ +export const isPointInterface = + new tg.IsInterface().withProperties({ x: tg.isNumber, y: tg.isNumber, direction: tg.isString, - moving: tg.isBoolean, - }) - .get(); + moving: tg.isBoolean + }).get(); export type PointInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/ProtobufUtils.ts b/back/src/Model/Websocket/ProtobufUtils.ts index 68817a4f..c31eb9a8 100644 --- a/back/src/Model/Websocket/ProtobufUtils.ts +++ b/back/src/Model/Websocket/ProtobufUtils.ts @@ -1,33 +1,34 @@ -import { PointInterface } from "./PointInterface"; +import {PointInterface} from "./PointInterface"; import { CharacterLayerMessage, ItemEventMessage, PointMessage, - PositionMessage, + PositionMessage } from "../../Messages/generated/messages_pb"; -import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; +import {CharacterLayer, ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import Direction = PositionMessage.Direction; -import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; -import { PositionInterface } from "_Model/PositionInterface"; +import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; +import {PositionInterface} from "_Model/PositionInterface"; export class ProtobufUtils { + public static toPositionMessage(point: PointInterface): PositionMessage { - let direction: Direction; + let direction: PositionMessage.DirectionMap[keyof PositionMessage.DirectionMap]; switch (point.direction) { - case "up": + case 'up': direction = Direction.UP; break; - case "down": + case 'down': direction = Direction.DOWN; break; - case "left": + case 'left': direction = Direction.LEFT; break; - case "right": + case 'right': direction = Direction.RIGHT; break; default: - throw new Error("unexpected direction"); + throw new Error('unexpected direction'); } const position = new PositionMessage(); @@ -43,16 +44,16 @@ export class ProtobufUtils { let direction: string; switch (position.getDirection()) { case Direction.UP: - direction = "up"; + direction = 'up'; break; case Direction.DOWN: - direction = "down"; + direction = 'down'; break; case Direction.LEFT: - direction = "left"; + direction = 'left'; break; case Direction.RIGHT: - direction = "right"; + direction = 'right'; break; default: throw new Error("Unexpected direction"); @@ -81,7 +82,7 @@ export class ProtobufUtils { event: itemEventMessage.getEvent(), parameters: JSON.parse(itemEventMessage.getParametersjson()), state: JSON.parse(itemEventMessage.getStatejson()), - }; + } } public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { @@ -95,7 +96,7 @@ export class ProtobufUtils { } public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { - return characterLayers.map(function (characterLayer): CharacterLayerMessage { + return characterLayers.map(function(characterLayer): CharacterLayerMessage { const message = new CharacterLayerMessage(); message.setName(characterLayer.name); if (characterLayer.url) { @@ -104,14 +105,4 @@ export class ProtobufUtils { return message; }); } - - public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] { - return characterLayers.map(function (characterLayer): CharacterLayer { - const url = characterLayer.getUrl(); - return { - name: characterLayer.getName(), - url: url ? url : undefined, - }; - }); - } } diff --git a/back/src/Model/Websocket/SetPlayerDetailsMessage.ts b/back/src/Model/Websocket/SetPlayerDetailsMessage.ts new file mode 100644 index 00000000..1693f9a0 --- /dev/null +++ b/back/src/Model/Websocket/SetPlayerDetailsMessage.ts @@ -0,0 +1,8 @@ +import * as tg from "generic-type-guard"; + +export const isSetPlayerDetailsMessage = + new tg.IsInterface().withProperties({ + name: tg.isString, + characterLayers: tg.isArray(tg.isString) + }).get(); +export type SetPlayerDetailsMessage = tg.GuardedType; diff --git a/back/src/Model/Websocket/UserInGroupInterface.ts b/back/src/Model/Websocket/UserInGroupInterface.ts new file mode 100644 index 00000000..087f519e --- /dev/null +++ b/back/src/Model/Websocket/UserInGroupInterface.ts @@ -0,0 +1,5 @@ +export interface UserInGroupInterface { + userId: number, + name: string, + initiator: boolean +} diff --git a/pusher/src/Model/Websocket/ViewportMessage.ts b/back/src/Model/Websocket/ViewportMessage.ts similarity index 71% rename from pusher/src/Model/Websocket/ViewportMessage.ts rename to back/src/Model/Websocket/ViewportMessage.ts index ea71ad68..62e2fc81 100644 --- a/pusher/src/Model/Websocket/ViewportMessage.ts +++ b/back/src/Model/Websocket/ViewportMessage.ts @@ -1,11 +1,10 @@ import * as tg from "generic-type-guard"; -export const isViewport = new tg.IsInterface() - .withProperties({ +export const isViewport = + new tg.IsInterface().withProperties({ left: tg.isNumber, top: tg.isNumber, right: tg.isNumber, bottom: tg.isNumber, - }) - .get(); + }).get(); export type ViewportInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/WebRtcSignalMessage.ts b/back/src/Model/Websocket/WebRtcSignalMessage.ts new file mode 100644 index 00000000..c0f5f8ab --- /dev/null +++ b/back/src/Model/Websocket/WebRtcSignalMessage.ts @@ -0,0 +1,18 @@ +import * as tg from "generic-type-guard"; + +export const isSignalData = + new tg.IsInterface().withProperties({ + type: tg.isOptional(tg.isString) + }).get(); + +export const isWebRtcSignalMessageInterface = + new tg.IsInterface().withProperties({ + receiverId: tg.isNumber, + signal: isSignalData + }).get(); +export const isWebRtcScreenSharingStartMessageInterface = + new tg.IsInterface().withProperties({ + userId: tg.isNumber, + roomId: tg.isString + }).get(); +export type WebRtcSignalMessageInterface = tg.GuardedType; diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index 5c3e92ba..4266c892 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -1,53 +1,36 @@ -import { User } from "./User"; -import { PositionInterface } from "_Model/PositionInterface"; -import { Movable } from "./Movable"; -import { Group } from "./Group"; -import { ZoneSocket } from "../RoomManager"; -import { - EmoteEventMessage, - SetPlayerDetailsMessage, - PlayerDetailsUpdatedMessage, -} from "../Messages/generated/messages_pb"; +import {User} from "./User"; +import {PositionInterface} from "_Model/PositionInterface"; +import {Movable} from "./Movable"; +import {Group} from "./Group"; -export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void; -export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; -export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void; -export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; -export type PlayerDetailsUpdatedCallback = ( - playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, - listener: ZoneSocket -) => void; +export type EntersCallback = (thing: Movable, listener: User) => void; +export type MovesCallback = (thing: Movable, position: PositionInterface, listener: User) => void; +export type LeavesCallback = (thing: Movable, listener: User) => void; export class Zone { private things: Set = new Set(); - private listeners: Set = new Set(); + private listeners: Set = new Set(); - constructor( - private onEnters: EntersCallback, - private onMoves: MovesCallback, - private onLeaves: LeavesCallback, - private onEmote: EmoteCallback, - private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback, - public readonly x: number, - public readonly y: number - ) {} + /** + * @param x For debugging purpose only + * @param y For debugging purpose only + */ + constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private x: number, private y: number) { + } /** * A user/thing leaves the zone */ - public leave(thing: Movable, newZone: Zone | null) { + public leave(thing: Movable, newZone: Zone|null) { const result = this.things.delete(thing); if (!result) { if (thing instanceof User) { - throw new Error(`Could not find user in zone ${thing.id}`); + throw new Error('Could not find user in zone '+thing.id); } if (thing instanceof Group) { - throw new Error( - `Could not find group ${thing.getId()} in zone (${this.x},${this.y}). Position of group: (${ - thing.getPosition().x - },${thing.getPosition().y})` - ); + throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')'); } + } this.notifyLeft(thing, newZone); } @@ -55,13 +38,15 @@ export class Zone { /** * Notify listeners of this zone that this user/thing left */ - private notifyLeft(thing: Movable, newZone: Zone | null) { + private notifyLeft(thing: Movable, newZone: Zone|null) { for (const listener of this.listeners) { - this.onLeaves(thing, newZone, listener); + if (listener !== thing && (newZone === null || !listener.listenedZones.has(newZone))) { + this.onLeaves(thing, listener); + } } } - public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { + public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { this.things.add(thing); this.notifyEnter(thing, oldZone, position); } @@ -69,9 +54,16 @@ export class Zone { /** * Notify listeners of this zone that this user entered */ - private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { + private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { for (const listener of this.listeners) { - this.onEnters(thing, oldZone, listener); + if (listener === thing) { + continue; + } + if (oldZone === null || !listener.listenedZones.has(oldZone)) { + this.onEnters(thing, listener); + } else { + this.onMoves(thing, position, listener); + } } } @@ -83,38 +75,35 @@ export class Zone { } for (const listener of this.listeners) { - //if (listener !== thing) { - this.onMoves(thing, position, listener); - //} + if (listener !== thing) { + this.onMoves(thing,position, listener); + } } } + public startListening(listener: User): void { + for (const thing of this.things) { + if (thing !== listener) { + this.onEnters(thing, listener); + } + } + + this.listeners.add(listener); + listener.listenedZones.add(this); + } + + public stopListening(listener: User): void { + for (const thing of this.things) { + if (thing !== listener) { + this.onLeaves(thing, listener); + } + } + + this.listeners.delete(listener); + listener.listenedZones.delete(this); + } + public getThings(): Set { return this.things; } - - public addListener(socket: ZoneSocket): void { - this.listeners.add(socket); - // TODO: here, we should trigger in some way the sending of current items - } - - public removeListener(socket: ZoneSocket): void { - this.listeners.delete(socket); - } - - public emitEmoteEvent(emoteEventMessage: EmoteEventMessage) { - for (const listener of this.listeners) { - this.onEmote(emoteEventMessage, listener); - } - } - - public updatePlayerDetails(user: User, playerDetails: SetPlayerDetailsMessage) { - const playerDetailsUpdatedMessage = new PlayerDetailsUpdatedMessage(); - playerDetailsUpdatedMessage.setUserid(user.id); - playerDetailsUpdatedMessage.setDetails(playerDetails); - - for (const listener of this.listeners) { - this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener); - } - } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts deleted file mode 100644 index d375fbd8..00000000 --- a/back/src/RoomManager.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb"; -import { - AdminGlobalMessage, - AdminMessage, - AdminPusherToBackMessage, - AdminRoomMessage, - BanMessage, - BanUserMessage, - BatchToPusherMessage, - BatchToPusherRoomMessage, - EmotePromptMessage, - FollowRequestMessage, - FollowConfirmationMessage, - FollowAbortMessage, - EmptyMessage, - ItemEventMessage, - JoinRoomMessage, - PlayGlobalMessage, - PusherToBackMessage, - QueryJitsiJwtMessage, - RefreshRoomPromptMessage, - RoomMessage, - SendUserMessage, - ServerToAdminClientMessage, - SetPlayerDetailsMessage, - SilentMessage, - UserMovesMessage, - VariableMessage, - WebRtcSignalToServerMessage, - WorldFullWarningToRoomMessage, - ZoneMessage, -} from "./Messages/generated/messages_pb"; -import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc"; -import { socketManager } from "./Services/SocketManager"; -import { emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket } from "./Services/MessageHelpers"; -import { User, UserSocket } from "./Model/User"; -import { GameRoom } from "./Model/GameRoom"; -import Debug from "debug"; -import { Admin } from "./Model/Admin"; - -const debug = Debug("roommanager"); - -export type AdminSocket = ServerDuplexStream; -export type ZoneSocket = ServerWritableStream; -export type RoomSocket = ServerWritableStream; - -const roomManager: IRoomManagerServer = { - joinRoom: (call: UserSocket): void => { - console.log("joinRoom called"); - - let room: GameRoom | null = null; - let user: User | null = null; - - call.on("data", (message: PusherToBackMessage) => { - (async () => { - try { - if (room === null || user === null) { - if (message.hasJoinroommessage()) { - socketManager - .handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage) - .then(({ room: gameRoom, user: myUser }) => { - if (call.writable) { - room = gameRoom; - user = myUser; - } else { - //Connection may have been closed before the init was finished, so we have to manually disconnect the user. - socketManager.leaveRoom(gameRoom, myUser); - } - }) - .catch((e) => emitError(call, e)); - } else { - throw new Error("The first message sent MUST be of type JoinRoomMessage"); - } - } else { - if (message.hasJoinroommessage()) { - throw new Error("Cannot call JoinRoomMessage twice!"); - } else if (message.hasUsermovesmessage()) { - socketManager.handleUserMovesMessage( - room, - user, - message.getUsermovesmessage() as UserMovesMessage - ); - } else if (message.hasSilentmessage()) { - socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); - } else if (message.hasItemeventmessage()) { - socketManager.handleItemEvent( - room, - user, - message.getItemeventmessage() as ItemEventMessage - ); - } else if (message.hasVariablemessage()) { - await socketManager.handleVariableEvent( - room, - user, - message.getVariablemessage() as VariableMessage - ); - } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo( - room, - user, - message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage - ); - } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing( - room, - user, - message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage - ); - } else if (message.hasQueryjitsijwtmessage()) { - socketManager.handleQueryJitsiJwtMessage( - user, - message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage - ); - } else if (message.hasEmotepromptmessage()) { - socketManager.handleEmoteEventMessage( - room, - user, - message.getEmotepromptmessage() as EmotePromptMessage - ); - } else if (message.hasFollowrequestmessage()) { - socketManager.handleFollowRequestMessage( - room, - user, - message.getFollowrequestmessage() as FollowRequestMessage - ); - } else if (message.hasFollowconfirmationmessage()) { - socketManager.handleFollowConfirmationMessage( - room, - user, - message.getFollowconfirmationmessage() as FollowConfirmationMessage - ); - } else if (message.hasFollowabortmessage()) { - socketManager.handleFollowAbortMessage( - room, - user, - message.getFollowabortmessage() as FollowAbortMessage - ); - } else if (message.hasSendusermessage()) { - const sendUserMessage = message.getSendusermessage(); - socketManager.handleSendUserMessage(user, sendUserMessage as SendUserMessage); - } else if (message.hasBanusermessage()) { - const banUserMessage = message.getBanusermessage(); - socketManager.handlerBanUserMessage(room, user, banUserMessage as BanUserMessage); - } else if (message.hasSetplayerdetailsmessage()) { - const setPlayerDetailsMessage = message.getSetplayerdetailsmessage(); - socketManager.handleSetPlayerDetails( - room, - user, - setPlayerDetailsMessage as SetPlayerDetailsMessage - ); - } else { - throw new Error("Unhandled message type"); - } - } - } catch (e) { - console.error(e); - emitError(call, e); - call.end(); - } - })().catch((e) => console.error(e)); - }); - - call.on("end", () => { - debug("joinRoom ended"); - if (user !== null && room !== null) { - socketManager.leaveRoom(room, user); - } - call.end(); - room = null; - user = null; - }); - - call.on("error", (err: Error) => { - console.error("An error occurred in joinRoom stream:", err); - }); - }, - - listenZone(call: ZoneSocket): void { - debug("listenZone called"); - const zoneMessage = call.request; - - socketManager - .addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()) - .catch((e) => { - emitErrorOnZoneSocket(call, e); - }); - - call.on("cancelled", () => { - debug("listenZone cancelled"); - socketManager - .removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()) - .catch((e) => console.error(e)); - call.end(); - }); - - call.on("close", () => { - debug("listenZone connection closed"); - socketManager - .removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()) - .catch((e) => console.error(e)); - }).on("error", (e) => { - console.error("An error occurred in listenZone stream:", e); - socketManager - .removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()) - .catch((e) => console.error(e)); - call.end(); - }); - }, - - listenRoom(call: RoomSocket): void { - debug("listenRoom called"); - const roomMessage = call.request; - - socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => { - emitErrorOnRoomSocket(call, e); - }); - - call.on("cancelled", () => { - debug("listenRoom cancelled"); - socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e)); - call.end(); - }); - - call.on("close", () => { - debug("listenRoom connection closed"); - socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e)); - }).on("error", (e) => { - console.error("An error occurred in listenRoom stream:", e); - socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e)); - call.end(); - }); - }, - - adminRoom(call: AdminSocket): void { - console.log("adminRoom called"); - - const admin = new Admin(call); - let room: GameRoom | null = null; - - call.on("data", (message: AdminPusherToBackMessage) => { - try { - if (room === null) { - if (message.hasSubscribetoroom()) { - const roomId = message.getSubscribetoroom(); - socketManager - .handleJoinAdminRoom(admin, roomId) - .then((gameRoom: GameRoom) => { - room = gameRoom; - }) - .catch((e) => console.error(e)); - } else { - throw new Error("The first message sent MUST be of type JoinRoomMessage"); - } - } - } catch (e) { - emitError(call, e); - call.end(); - } - }); - - call.on("end", () => { - debug("joinRoom ended"); - if (room !== null) { - socketManager.leaveAdminRoom(room, admin); - } - call.end(); - room = null; - }); - - call.on("error", (err: Error) => { - console.error("An error occurred in joinAdminRoom stream:", err); - }); - }, - sendAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - socketManager - .sendAdminMessage( - call.request.getRoomid(), - call.request.getRecipientuuid(), - call.request.getMessage(), - call.request.getType() - ) - .catch((e) => console.error(e)); - - callback(null, new EmptyMessage()); - }, - sendGlobalAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - throw new Error("Not implemented yet"); - // TODO - callback(null, new EmptyMessage()); - }, - ban(call: ServerUnaryCall, callback: sendUnaryData): void { - // FIXME Work in progress - socketManager - .banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage()) - .catch((e) => console.error(e)); - - callback(null, new EmptyMessage()); - }, - sendAdminMessageToRoom(call: ServerUnaryCall, callback: sendUnaryData): void { - // FIXME: we could improve return message by returning a Success|ErrorMessage message - socketManager - .sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage(), call.request.getType()) - .catch((e) => console.error(e)); - callback(null, new EmptyMessage()); - }, - sendWorldFullWarningToRoom( - call: ServerUnaryCall, - callback: sendUnaryData - ): void { - // FIXME: we could improve return message by returning a Success|ErrorMessage message - socketManager.dispatchWorldFullWarning(call.request.getRoomid()).catch((e) => console.error(e)); - callback(null, new EmptyMessage()); - }, - sendRefreshRoomPrompt( - call: ServerUnaryCall, - callback: sendUnaryData - ): void { - // FIXME: we could improve return message by returning a Success|ErrorMessage message - socketManager.dispatchRoomRefresh(call.request.getRoomid()).catch((e) => console.error(e)); - callback(null, new EmptyMessage()); - }, -}; - -export { roomManager }; diff --git a/back/src/Server/server/app.ts b/back/src/Server/server/app.ts index 4c422d5c..3b98a9b3 100644 --- a/back/src/Server/server/app.ts +++ b/back/src/Server/server/app.ts @@ -1,13 +1,13 @@ -import { App as _App, AppOptions } from "uWebSockets.js"; -import BaseApp from "./baseapp"; -import { extend } from "./utils"; -import { UwsApp } from "./types"; +import { App as _App, AppOptions } from 'uWebSockets.js'; +import BaseApp from './baseapp'; +import { extend } from './utils'; +import { UwsApp } from './types'; class App extends (_App) { - constructor(options: AppOptions = {}) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions = {}) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default App; diff --git a/back/src/Server/server/baseapp.ts b/back/src/Server/server/baseapp.ts index d1d89d5b..accd8a99 100644 --- a/back/src/Server/server/baseapp.ts +++ b/back/src/Server/server/baseapp.ts @@ -1,111 +1,116 @@ -/* eslint-disable */ +import { Readable } from 'stream'; +import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; -import { Readable } from "stream"; -import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; +import formData from './formdata'; +import { stob } from './utils'; +import { Handler } from './types'; +import {join} from "path"; -import formData from "./formdata"; -import { stob } from "./utils"; -import { Handler } from "./types"; -import { join } from "path"; - -const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"]; +const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; const noOp = () => true; const handleBody = (res: HttpResponse, req: HttpRequest) => { - const contType = req.getHeader("content-type"); + const contType = req.getHeader('content-type'); - res.bodyStream = function () { - const stream = new Readable(); - stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method + res.bodyStream = function() { + const stream = new Readable(); + stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method - this.onData((ab: ArrayBuffer, isLast: boolean) => { - // uint and then slicing is bit faster than slice and then uint - stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any - if (isLast) { - stream.push(null); - } - }); + this.onData((ab: ArrayBuffer, isLast: boolean) => { + // uint and then slicing is bit faster than slice and then uint + stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any + if (isLast) { + stream.push(null); + } + }); - return stream; - }; + return stream; + }; - res.body = () => stob(res.bodyStream()); + res.body = () => stob(res.bodyStream()); - if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body()); - if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType); + if (contType.includes('application/json')) + res.json = async () => JSON.parse(await res.body()); + if (contTypes.map(t => contType.includes(t)).includes(true)) + res.formData = formData.bind(res, contType); }; class BaseApp { - _sockets = new Map(); - ws!: TemplatedApp["ws"]; - get!: TemplatedApp["get"]; - _post!: TemplatedApp["post"]; - _put!: TemplatedApp["put"]; - _patch!: TemplatedApp["patch"]; - _listen!: TemplatedApp["listen"]; + _sockets = new Map(); + ws!: TemplatedApp['ws']; + get!: TemplatedApp['get']; + _post!: TemplatedApp['post']; + _put!: TemplatedApp['put']; + _patch!: TemplatedApp['patch']; + _listen!: TemplatedApp['listen']; - post(pattern: string, handler: Handler) { - if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); - this._post(pattern, (res, req) => { - handleBody(res, req); - handler(res, req); - }); - return this; - } + post(pattern: string, handler: Handler) { + if (typeof handler !== 'function') + throw Error(`handler should be a function, given ${typeof handler}.`); + this._post(pattern, (res, req) => { + handleBody(res, req); + handler(res, req); + }); + return this; + } - put(pattern: string, handler: Handler) { - if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); - this._put(pattern, (res, req) => { - handleBody(res, req); + put(pattern: string, handler: Handler) { + if (typeof handler !== 'function') + throw Error(`handler should be a function, given ${typeof handler}.`); + this._put(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - patch(pattern: string, handler: Handler) { - if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); - this._patch(pattern, (res, req) => { - handleBody(res, req); + patch(pattern: string, handler: Handler) { + if (typeof handler !== 'function') + throw Error(`handler should be a function, given ${typeof handler}.`); + this._patch(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - listen(h: string | number, p: Function | number = noOp, cb?: Function) { - if (typeof p === "number" && typeof h === "string") { - this._listen(h, p, (socket) => { - this._sockets.set(p, socket); - if (cb === undefined) { - throw new Error("cb undefined"); - } - cb(socket); - }); - } else if (typeof h === "number" && typeof p === "function") { - this._listen(h, (socket) => { - this._sockets.set(h, socket); - p(socket); - }); - } else { - throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)"); + listen(h: string | number, p: Function | number = noOp, cb?: Function) { + if (typeof p === 'number' && typeof h === 'string') { + this._listen(h, p, socket => { + this._sockets.set(p, socket); + if (cb === undefined) { + throw new Error('cb undefined'); } - - return this; + cb(socket); + }); + } else if (typeof h === 'number' && typeof p === 'function') { + this._listen(h, socket => { + this._sockets.set(h, socket); + p(socket); + }); + } else { + throw Error( + 'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)' + ); } - close(port: null | number = null) { - if (port) { - this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); - this._sockets.delete(port); - } else { - this._sockets.forEach((app) => { - us_listen_socket_close(app); - }); - this._sockets.clear(); - } - return this; + return this; + } + + close(port: null | number = null) { + if (port) { + this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); + this._sockets.delete(port); + } else { + this._sockets.forEach(app => { + us_listen_socket_close(app); + }); + this._sockets.clear(); } + return this; + } } export default BaseApp; diff --git a/back/src/Server/server/formdata.ts b/back/src/Server/server/formdata.ts index b0da02cd..9dd08440 100644 --- a/back/src/Server/server/formdata.ts +++ b/back/src/Server/server/formdata.ts @@ -1,101 +1,100 @@ -/* eslint-disable */ - -import { createWriteStream } from "fs"; -import { join, dirname } from "path"; -import Busboy from "busboy"; -import mkdirp from "mkdirp"; +import { createWriteStream } from 'fs'; +import { join, dirname } from 'path'; +import Busboy from 'busboy'; +import mkdirp from 'mkdirp'; function formData( - contType: string, - options: busboy.BusboyConfig & { - abortOnLimit?: boolean; - tmpDir?: string; - onFile?: ( - fieldname: string, - file: NodeJS.ReadableStream, - filename: string, - encoding: string, - mimetype: string - ) => string; - onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - filename?: (oldName: string) => string; - } = {} + contType: string, + options: busboy.BusboyConfig & { + abortOnLimit?: boolean; + tmpDir?: string; + onFile?: ( + fieldname: string, + file: NodeJS.ReadableStream, + filename: string, + encoding: string, + mimetype: string + ) => string; + onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + filename?: (oldName: string) => string; + } = {} ) { - console.log("Enter form data"); - options.headers = { - "content-type": contType, - }; + console.log('Enter form data'); + options.headers = { + 'content-type': contType + }; - return new Promise((resolve, reject) => { - const busb = new Busboy(options); - const ret = {}; + return new Promise((resolve, reject) => { + const busb = new Busboy(options); + const ret = {}; - this.bodyStream().pipe(busb); + this.bodyStream().pipe(busb); - busb.on("limit", () => { - if (options.abortOnLimit) { - reject(Error("limit")); - } - }); - - busb.on("file", function (fieldname, file, filename, encoding, mimetype) { - const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = { - filename, - encoding, - mimetype, - filePath: undefined, - }; - - if (typeof options.tmpDir === "string") { - if (typeof options.filename === "function") filename = options.filename(filename); - const fileToSave = join(options.tmpDir, filename); - mkdirp(dirname(fileToSave)); - - file.pipe(createWriteStream(fileToSave)); - value.filePath = fileToSave; - } - if (typeof options.onFile === "function") { - value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; - } - - setRetValue(ret, fieldname, value); - }); - - busb.on("field", function (fieldname, value) { - if (typeof options.onField === "function") options.onField(fieldname, value); - - setRetValue(ret, fieldname, value); - }); - - busb.on("finish", function () { - resolve(ret); - }); - - busb.on("error", reject); + busb.on('limit', () => { + if (options.abortOnLimit) { + reject(Error('limit')); + } }); + + busb.on('file', function(fieldname, file, filename, encoding, mimetype) { + const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = { + filename, + encoding, + mimetype, + filePath: undefined + }; + + if (typeof options.tmpDir === 'string') { + if (typeof options.filename === 'function') filename = options.filename(filename); + const fileToSave = join(options.tmpDir, filename); + mkdirp(dirname(fileToSave)); + + file.pipe(createWriteStream(fileToSave)); + value.filePath = fileToSave; + } + if (typeof options.onFile === 'function') { + value.filePath = + options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; + } + + setRetValue(ret, fieldname, value); + }); + + busb.on('field', function(fieldname, value) { + if (typeof options.onField === 'function') options.onField(fieldname, value); + + setRetValue(ret, fieldname, value); + }); + + busb.on('finish', function() { + resolve(ret); + }); + + busb.on('error', reject); + }); } function setRetValue( - ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any - fieldname: string, - value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any + ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + fieldname: string, + value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any ) { - if (fieldname.endsWith("[]")) { - fieldname = fieldname.slice(0, fieldname.length - 2); - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); - } else { - ret[fieldname] = [value]; - } + if (fieldname.endsWith('[]')) { + fieldname = fieldname.slice(0, fieldname.length - 2); + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); } else { - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); - } else if (ret[fieldname]) { - ret[fieldname] = [ret[fieldname], value]; - } else { - ret[fieldname] = value; - } + ret[fieldname] = [value]; } + } else { + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else if (ret[fieldname]) { + ret[fieldname] = [ret[fieldname], value]; + } else { + ret[fieldname] = value; + } + } } export default formData; diff --git a/back/src/Server/server/sslapp.ts b/back/src/Server/server/sslapp.ts index 80df0e4a..46ae89a5 100644 --- a/back/src/Server/server/sslapp.ts +++ b/back/src/Server/server/sslapp.ts @@ -1,13 +1,13 @@ -import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js"; -import BaseApp from "./baseapp"; -import { extend } from "./utils"; -import { UwsApp } from "./types"; +import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; +import BaseApp from './baseapp'; +import { extend } from './utils'; +import { UwsApp } from './types'; class SSLApp extends (_SSLApp) { - constructor(options: AppOptions) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default SSLApp; diff --git a/back/src/Server/server/types.ts b/back/src/Server/server/types.ts index afc21d17..3d0f48c7 100644 --- a/back/src/Server/server/types.ts +++ b/back/src/Server/server/types.ts @@ -1,9 +1,9 @@ -import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; +import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; export type UwsApp = { - (options: AppOptions): TemplatedApp; - new (options: AppOptions): TemplatedApp; - prototype: TemplatedApp; + (options: AppOptions): TemplatedApp; + new (options: AppOptions): TemplatedApp; + prototype: TemplatedApp; }; export type Handler = (res: HttpResponse, req: HttpRequest) => void; diff --git a/back/src/Server/server/utils.ts b/back/src/Server/server/utils.ts index f475c27d..80ea3938 100644 --- a/back/src/Server/server/utils.ts +++ b/back/src/Server/server/utils.ts @@ -1,38 +1,37 @@ -/* eslint-disable */ +import { ReadStream } from 'fs'; -import { ReadStream } from "fs"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function extend(who: any, from: any, overwrite = true) { - const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); - ownProps.forEach((prop) => { - if (prop === "constructor" || from[prop] === undefined) return; - if (who[prop] && overwrite) { - who[`_${prop}`] = who[prop]; - } - if (typeof from[prop] === "function") who[prop] = from[prop].bind(who); - else who[prop] = from[prop]; - }); +function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any + const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( + Object.keys(from) + ); + ownProps.forEach(prop => { + if (prop === 'constructor' || from[prop] === undefined) return; + if (who[prop] && overwrite) { + who[`_${prop}`] = who[prop]; + } + if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); + else who[prop] = from[prop]; + }); } function stob(stream: ReadStream): Promise { - return new Promise((resolve) => { - const buffers: Buffer[] = []; - stream.on("data", buffers.push.bind(buffers)); + return new Promise(resolve => { + const buffers: Buffer[] = []; + stream.on('data', buffers.push.bind(buffers)); - stream.on("end", () => { - switch (buffers.length) { - case 0: - resolve(Buffer.allocUnsafe(0)); - break; - case 1: - resolve(buffers[0]); - break; - default: - resolve(Buffer.concat(buffers)); - } - }); + stream.on('end', () => { + switch (buffers.length) { + case 0: + resolve(Buffer.allocUnsafe(0)); + break; + case 1: + resolve(buffers[0]); + break; + default: + resolve(Buffer.concat(buffers)); + } }); + }); } export { extend, stob }; diff --git a/back/src/Server/sifrr.server.ts b/back/src/Server/sifrr.server.ts index 4ef03721..47fba02c 100644 --- a/back/src/Server/sifrr.server.ts +++ b/back/src/Server/sifrr.server.ts @@ -1,19 +1,19 @@ -import { parse } from "query-string"; -import { HttpRequest } from "uWebSockets.js"; -import App from "./server/app"; -import SSLApp from "./server/sslapp"; -import * as types from "./server/types"; +import { parse } from 'query-string'; +import { HttpRequest } from 'uWebSockets.js'; +import App from './server/app'; +import SSLApp from './server/sslapp'; +import * as types from './server/types'; const getQuery = (req: HttpRequest) => { - return parse(req.getQuery()); + return parse(req.getQuery()); }; export { App, SSLApp, getQuery }; -export * from "./server/types"; +export * from './server/types'; export default { - App, - SSLApp, - getQuery, - ...types, + App, + SSLApp, + getQuery, + ...types }; diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index f4fa40b6..9c46a41b 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -1,30 +1,115 @@ -import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable"; +import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; import Axios from "axios"; -import { isMapDetailsData, MapDetailsData } from "./AdminApi/MapDetailsData"; -import { isRoomRedirect, RoomRedirect } from "./AdminApi/RoomRedirect"; +import {v4} from "uuid"; + +export interface AdminApiData { + organizationSlug: string + worldSlug: string + roomSlug: string + mapUrlStart: string + tags: string[] + policy_type: number + userUuid: string + messages?: unknown[], + textures: CharacterTexture[] +} + +export interface CharacterTexture { + id: number, + level: number, + url: string, + rights: string +} + +export interface FetchMemberDataByUuidResponse { + uuid: string; + tags: string[]; + textures: CharacterTexture[]; + messages: unknown[]; +} class AdminApi { - async fetchMapDetails(playUri: string): Promise { + + async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error("No admin backoffice set!")); + return Promise.reject('No admin backoffice set!'); } - const params: { playUri: string } = { - playUri, + const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { + organizationSlug, + worldSlug }; - const res = await Axios.get(ADMIN_API_URL + "/api/map", { - headers: { Authorization: `${ADMIN_API_TOKEN}` }, - params, - }); - - if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) { - console.error("Unexpected answer from the /api/map admin endpoint.", res.data); - throw new Error("Unexpected answer from the /api/map admin endpoint."); + if (roomSlug) { + params.roomSlug = roomSlug; } + const res = await Axios.get(ADMIN_API_URL + '/api/map', + { + headers: {"Authorization": `${ADMIN_API_TOKEN}`}, + params + } + ) return res.data; } + + async fetchMemberDataByUuid(uuid: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('No admin backoffice set!'); + } + try { + const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data; + } catch (e) { + if (e?.response?.status == 404) { + // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! + console.warn('Cannot find user with uuid "'+uuid+'". Performing an anonymous login instead.'); + return { + uuid: v4(), + tags: [], + textures: [], + messages: [], + } + } else { + throw e; + } + } + } + + async fetchMemberDataByToken(organizationMemberToken: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('No admin backoffice set!'); + } + //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. + const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data; + } + + async fetchCheckUserByToken(organizationMemberToken: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('No admin backoffice set!'); + } + //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. + const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data; + } + + reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) { + return Axios.post(`${ADMIN_API_URL}/api/report`, { + reportedUserUuid, + reportedUserComment, + reporterUserUuid, + }, + { + headers: {"Authorization": `${ADMIN_API_TOKEN}`} + }); + } } export const adminApi = new AdminApi(); diff --git a/back/src/Services/AdminApi/CharacterTexture.ts b/back/src/Services/AdminApi/CharacterTexture.ts deleted file mode 100644 index 055b3033..00000000 --- a/back/src/Services/AdminApi/CharacterTexture.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as tg from "generic-type-guard"; - -export const isCharacterTexture = new tg.IsInterface() - .withProperties({ - id: tg.isNumber, - level: tg.isNumber, - url: tg.isString, - rights: tg.isString, - }) - .get(); -export type CharacterTexture = tg.GuardedType; diff --git a/back/src/Services/AdminApi/MapDetailsData.ts b/back/src/Services/AdminApi/MapDetailsData.ts deleted file mode 100644 index d3402b92..00000000 --- a/back/src/Services/AdminApi/MapDetailsData.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as tg from "generic-type-guard"; -import { isCharacterTexture } from "./CharacterTexture"; -import { isAny, isNumber } from "generic-type-guard"; - -/*const isNumericEnum = - (vs: T) => - (v: any): v is T => - typeof v === "number" && v in vs;*/ - -export const isMapDetailsData = new tg.IsInterface() - .withProperties({ - mapUrl: tg.isString, - policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes), - tags: tg.isArray(tg.isString), - textures: tg.isArray(isCharacterTexture), - }) - .withOptionalProperties({ - roomSlug: tg.isUnion(tg.isString, tg.isNull), // deprecated - }) - .get(); -export type MapDetailsData = tg.GuardedType; diff --git a/back/src/Services/AdminApi/RoomRedirect.ts b/back/src/Services/AdminApi/RoomRedirect.ts deleted file mode 100644 index 7257ebd3..00000000 --- a/back/src/Services/AdminApi/RoomRedirect.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as tg from "generic-type-guard"; - -export const isRoomRedirect = new tg.IsInterface() - .withProperties({ - redirectUrl: tg.isString, - }) - .get(); -export type RoomRedirect = tg.GuardedType; diff --git a/back/src/Services/ArrayHelper.ts b/back/src/Services/ArrayHelper.ts index 8af1da9f..67321d1b 100644 --- a/back/src/Services/ArrayHelper.ts +++ b/back/src/Services/ArrayHelper.ts @@ -1,3 +1,3 @@ -export const arrayIntersect = (array1: string[], array2: string[]): boolean => { - return array1.filter((value) => array2.includes(value)).length > 0; -}; +export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { + return array1.filter(value => array2.includes(value)).length > 0; +} \ No newline at end of file diff --git a/back/src/Services/ClientEventsEmitter.ts b/back/src/Services/ClientEventsEmitter.ts index df920166..7b888ef6 100644 --- a/back/src/Services/ClientEventsEmitter.ts +++ b/back/src/Services/ClientEventsEmitter.ts @@ -1,7 +1,7 @@ -import { EventEmitter } from "events"; +const EventEmitter = require('events'); -const clientJoinEvent = "clientJoin"; -const clientLeaveEvent = "clientLeave"; +const clientJoinEvent = 'clientJoin'; +const clientLeaveEvent = 'clientLeave'; class ClientEventsEmitter extends EventEmitter { emitClientJoin(clientUUid: string, roomId: string): void { @@ -11,7 +11,7 @@ class ClientEventsEmitter extends EventEmitter { emitClientLeave(clientUUid: string, roomId: string): void { this.emit(clientLeaveEvent, clientUUid, roomId); } - + registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void { this.on(clientJoinEvent, callback); } @@ -29,4 +29,4 @@ class ClientEventsEmitter extends EventEmitter { } } -export const clientEventsEmitter = new ClientEventsEmitter(); +export const clientEventsEmitter = new ClientEventsEmitter(); \ No newline at end of file diff --git a/back/src/Services/CpuTracker.ts b/back/src/Services/CpuTracker.ts index 3d06ca70..c7d57f3d 100644 --- a/back/src/Services/CpuTracker.ts +++ b/back/src/Services/CpuTracker.ts @@ -1,6 +1,6 @@ -import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable"; +import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; -function secNSec2ms(secNSec: Array | number) { +function secNSec2ms(secNSec: Array|number) { if (Array.isArray(secNSec)) { return secNSec[0] * 1000 + secNSec[1] / 1000000; } @@ -12,17 +12,17 @@ class CpuTracker { private overHeating: boolean = false; constructor() { - let time = process.hrtime.bigint(); - let usage = process.cpuUsage(); + let time = process.hrtime.bigint() + let usage = process.cpuUsage() setInterval(() => { const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage); - usage = process.cpuUsage(); + const elapUsage = process.cpuUsage(usage) + usage = process.cpuUsage() const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user); - const elapSystMS = secNSec2ms(elapUsage.system); - this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000); + const elapUserMS = secNSec2ms(elapUsage.user) + const elapSystMS = secNSec2ms(elapUsage.system) + this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) time = elapTime; diff --git a/back/src/Services/GaugeManager.ts b/back/src/Services/GaugeManager.ts index 740692a8..f8af822b 100644 --- a/back/src/Services/GaugeManager.ts +++ b/back/src/Services/GaugeManager.ts @@ -1,4 +1,4 @@ -import { Counter, Gauge } from "prom-client"; +import {Counter, Gauge} from "prom-client"; //this class should manage all the custom metrics used by prometheus class GaugeManager { @@ -6,43 +6,31 @@ class GaugeManager { private nbClientsPerRoomGauge: Gauge; private nbGroupsPerRoomGauge: Gauge; private nbGroupsPerRoomCounter: Counter; - private nbRoomsGauge: Gauge; constructor() { - this.nbRoomsGauge = new Gauge({ - name: "workadventure_nb_rooms", - help: "Number of active rooms", - }); this.nbClientsGauge = new Gauge({ - name: "workadventure_nb_sockets", - help: "Number of connected sockets", - labelNames: [], + name: 'workadventure_nb_sockets', + help: 'Number of connected sockets', + labelNames: [ ] }); this.nbClientsPerRoomGauge = new Gauge({ - name: "workadventure_nb_clients_per_room", - help: "Number of clients per room", - labelNames: ["room"], + name: 'workadventure_nb_clients_per_room', + help: 'Number of clients per room', + labelNames: [ 'room' ] }); this.nbGroupsPerRoomCounter = new Counter({ - name: "workadventure_counter_groups_per_room", - help: "Counter of groups per room", - labelNames: ["room"], + name: 'workadventure_counter_groups_per_room', + help: 'Counter of groups per room', + labelNames: [ 'room' ] }); this.nbGroupsPerRoomGauge = new Gauge({ - name: "workadventure_nb_groups_per_room", - help: "Number of groups per room", - labelNames: ["room"], + name: 'workadventure_nb_groups_per_room', + help: 'Number of groups per room', + labelNames: [ 'room' ] }); } - incNbRoomGauge(): void { - this.nbRoomsGauge.inc(); - } - decNbRoomGauge(): void { - this.nbRoomsGauge.dec(); - } - incNbClientPerRoomGauge(roomId: string): void { this.nbClientsGauge.inc(); this.nbClientsPerRoomGauge.inc({ room: roomId }); @@ -52,6 +40,15 @@ class GaugeManager { this.nbClientsGauge.dec(); this.nbClientsPerRoomGauge.dec({ room: roomId }); } + + incNbGroupsPerRoomGauge(roomId: string): void { + this.nbGroupsPerRoomCounter.inc({ room: roomId }) + this.nbGroupsPerRoomGauge.inc({ room: roomId }) + } + + decNbGroupsPerRoomGauge(roomId: string): void { + this.nbGroupsPerRoomGauge.dec({ room: roomId }) + } } -export const gaugeManager = new GaugeManager(); +export const gaugeManager = new GaugeManager(); \ No newline at end of file diff --git a/pusher/src/Services/IoSocketHelpers.ts b/back/src/Services/IoSocketHelpers.ts similarity index 60% rename from pusher/src/Services/IoSocketHelpers.ts rename to back/src/Services/IoSocketHelpers.ts index 2da7c430..2166a53e 100644 --- a/pusher/src/Services/IoSocketHelpers.ts +++ b/back/src/Services/IoSocketHelpers.ts @@ -1,6 +1,5 @@ -import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; -import { BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage } from "../Messages/generated/messages_pb"; -import { WebSocket } from "uWebSockets.js"; +import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import {BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -19,9 +18,25 @@ export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): voi socket.batchTimeout = null; }, 100); } + + // If we send a message, we don't need to keep the connection alive + resetPing(socket); } -export function emitError(Client: WebSocket, message: string): void { +export function resetPing(ws: ExSocketInterface): void { + if (ws.pingTimeout) { + clearTimeout(ws.pingTimeout); + } + ws.pingTimeout = setTimeout(() => { + if (ws.disconnecting) { + return; + } + ws.ping(); + resetPing(ws); + }, 29000); +} + +export function emitError(Client: ExSocketInterface, message: string): void { const errorMessage = new ErrorMessage(); errorMessage.setMessage(message); @@ -32,4 +47,4 @@ export function emitError(Client: WebSocket, message: string): void { Client.send(serverToClientMessage.serializeBinary().buffer, true); } console.warn(message); -} +} \ No newline at end of file diff --git a/back/src/Services/JWTTokenManager.ts b/back/src/Services/JWTTokenManager.ts new file mode 100644 index 00000000..f82fa001 --- /dev/null +++ b/back/src/Services/JWTTokenManager.ts @@ -0,0 +1,72 @@ +import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; +import {uuid} from "uuidv4"; +import Jwt from "jsonwebtoken"; +import {TokenInterface} from "../Controller/AuthenticateController"; +import {adminApi, AdminApiData} from "../Services/AdminApi"; + +class JWTTokenManager { + + public createJWTToken(userUuid: string) { + return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token + } + + public async getUserUuidFromToken(token: unknown): Promise { + + if (!token) { + throw new Error('An authentication error happened, a user tried to connect without a token.'); + } + if (typeof(token) !== "string") { + throw new Error('Token is expected to be a string'); + } + + + if(token === 'test') { + if (ALLOW_ARTILLERY) { + return uuid(); + } else { + throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); + } + } + + return new Promise((resolve, reject) => { + Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { + const tokenInterface = tokenDecoded as TokenInterface; + if (err) { + console.error('An authentication error happened, invalid JsonWebToken.', err); + reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message)); + return; + } + if (tokenDecoded === undefined) { + console.error('Empty token found.'); + reject(new Error('Empty token found.')); + return; + } + + //verify token + if (!this.isValidToken(tokenInterface)) { + reject(new Error('Authentication error, invalid token structure.')); + return; + } + + //verify user in admin + adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => { + resolve(tokenInterface.userUuid); + }).catch((err) => { + //anonymous user + if(err.response && err.response.status && err.response.status === 404){ + resolve(tokenInterface.userUuid); + return; + } + reject(new Error('Authentication error, invalid token structure. ' + err)); + }); + }); + }); + } + + private isValidToken(token: object): token is TokenInterface { + return !(typeof((token as TokenInterface).userUuid) !== 'string'); + } + +} + +export const jwtTokenManager = new JWTTokenManager(); \ No newline at end of file diff --git a/back/src/Services/LocalUrlError.ts b/back/src/Services/LocalUrlError.ts deleted file mode 100644 index a4984fdd..00000000 --- a/back/src/Services/LocalUrlError.ts +++ /dev/null @@ -1 +0,0 @@ -export class LocalUrlError extends Error {} diff --git a/back/src/Services/Logger.ts b/back/src/Services/Logger.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/back/src/Services/MapFetcher.ts b/back/src/Services/MapFetcher.ts deleted file mode 100644 index 813685b2..00000000 --- a/back/src/Services/MapFetcher.ts +++ /dev/null @@ -1,70 +0,0 @@ -import Axios from "axios"; -import ipaddr from "ipaddr.js"; -import { Resolver } from "dns"; -import { promisify } from "util"; -import { LocalUrlError } from "./LocalUrlError"; -import { ITiledMap } from "@workadventure/tiled-map-type-guard"; -import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist"; -import { STORE_VARIABLES_FOR_LOCAL_MAPS } from "../Enum/EnvironmentVariable"; - -class MapFetcher { - async fetchMap(mapUrl: string): Promise { - // Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map) - - if ((await this.isLocalUrl(mapUrl)) && !STORE_VARIABLES_FOR_LOCAL_MAPS) { - throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map'); - } - - // Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that - // returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially - // target to different servers (and one could trick Axios.get into loading resources on the internal network - // despite isLocalUrl checking that. - // We can deem this problem not that important because: - // - We make sure we are only passing "GET" requests - // - The result of the query is never displayed to the end user - const res = await Axios.get(mapUrl, { - maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger - timeout: 10000, // Timeout after 10 seconds - }); - - if (!isTiledMap(res.data)) { - //TODO fixme - //throw new Error("Invalid map format for map " + mapUrl); - console.error("Invalid map format for map " + mapUrl); - } - /* eslint-disable-next-line @typescript-eslint/no-unsafe-return */ - return res.data; - } - - /** - * Returns true if the domain name is localhost of *.localhost - * Returns true if the domain name resolves to an IP address that is "private" (like 10.x.x.x or 192.168.x.x) - * - * @private - */ - async isLocalUrl(url: string): Promise { - const urlObj = new URL(url); - if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) { - return true; - } - - let addresses = []; - if (!ipaddr.isValid(urlObj.hostname)) { - const resolver = new Resolver(); - addresses = await promisify(resolver.resolve).bind(resolver)(urlObj.hostname); - } else { - addresses = [urlObj.hostname]; - } - - for (const address of addresses) { - const addr = ipaddr.parse(address); - if (addr.range() !== "unicast") { - return true; - } - } - - return false; - } -} - -export const mapFetcher = new MapFetcher(); diff --git a/back/src/Services/MessageHelpers.ts b/back/src/Services/MessageHelpers.ts deleted file mode 100644 index 34edc473..00000000 --- a/back/src/Services/MessageHelpers.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - BatchMessage, - BatchToPusherMessage, - BatchToPusherRoomMessage, - ErrorMessage, - ServerToClientMessage, - SubToPusherMessage, - SubToPusherRoomMessage, -} from "../Messages/generated/messages_pb"; -import { UserSocket } from "_Model/User"; -import { RoomSocket, ZoneSocket } from "../RoomManager"; - -function getMessageFromError(error: unknown): string { - if (error instanceof Error) { - return error.message; - } else if (typeof error === "string") { - return error; - } else { - return "Unknown error"; - } -} - -export function emitError(Client: UserSocket, error: unknown): void { - const message = getMessageFromError(error); - - const errorMessage = new ErrorMessage(); - errorMessage.setMessage(message); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setErrormessage(errorMessage); - - //if (!Client.disconnecting) { - Client.write(serverToClientMessage); - //} - console.warn(message); -} - -export function emitErrorOnRoomSocket(Client: RoomSocket, error: unknown): void { - console.error(error); - const message = getMessageFromError(error); - - const errorMessage = new ErrorMessage(); - errorMessage.setMessage(message); - - const subToPusherRoomMessage = new SubToPusherRoomMessage(); - subToPusherRoomMessage.setErrormessage(errorMessage); - - const batchToPusherMessage = new BatchToPusherRoomMessage(); - batchToPusherMessage.addPayload(subToPusherRoomMessage); - - //if (!Client.disconnecting) { - Client.write(batchToPusherMessage); - //} - console.warn(message); -} - -export function emitErrorOnZoneSocket(Client: ZoneSocket, error: unknown): void { - console.error(error); - const message = getMessageFromError(error); - - const errorMessage = new ErrorMessage(); - errorMessage.setMessage(message); - - const subToPusherMessage = new SubToPusherMessage(); - subToPusherMessage.setErrormessage(errorMessage); - - const batchToPusherMessage = new BatchToPusherMessage(); - batchToPusherMessage.addPayload(subToPusherMessage); - - //if (!Client.disconnecting) { - Client.write(batchToPusherMessage); - //} - console.warn(message); -} diff --git a/back/src/Services/RedisClient.ts b/back/src/Services/RedisClient.ts deleted file mode 100644 index 1f8c1ecd..00000000 --- a/back/src/Services/RedisClient.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ClientOpts, createClient, RedisClient } from "redis"; -import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from "../Enum/EnvironmentVariable"; - -let redisClient: RedisClient | null = null; - -if (REDIS_HOST !== undefined) { - const config: ClientOpts = { - host: REDIS_HOST, - port: REDIS_PORT, - }; - - if (REDIS_PASSWORD) { - config.password = REDIS_PASSWORD; - } - - redisClient = createClient(config); - - redisClient.on("error", (err) => { - console.error("Error connecting to Redis:", err); - }); -} - -export { redisClient }; diff --git a/back/src/Services/Repository/RedisVariablesRepository.ts b/back/src/Services/Repository/RedisVariablesRepository.ts deleted file mode 100644 index 95d757ca..00000000 --- a/back/src/Services/Repository/RedisVariablesRepository.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { promisify } from "util"; -import { RedisClient } from "redis"; -import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface"; - -/** - * Class in charge of saving/loading variables from the data store - */ -export class RedisVariablesRepository implements VariablesRepositoryInterface { - private readonly hgetall: OmitThisParameter<(arg1: string) => Promise<{ [p: string]: string }>>; - private readonly hset: OmitThisParameter<(arg1: [string, ...string[]]) => Promise>; - private readonly hdel: OmitThisParameter<(arg1: string, arg2: string) => Promise>; - - constructor(private redisClient: RedisClient) { - /* eslint-disable @typescript-eslint/unbound-method */ - this.hgetall = promisify(redisClient.hgetall).bind(redisClient); - this.hset = promisify(redisClient.hset).bind(redisClient); - this.hdel = promisify(redisClient.hdel).bind(redisClient); - /* eslint-enable @typescript-eslint/unbound-method */ - } - - /** - * Load all variables for a room. - * - * Note: in Redis, variables are stored in a hashmap and the key is the roomUrl - */ - async loadVariables(roomUrl: string): Promise<{ [key: string]: string }> { - return this.hgetall(roomUrl); - } - - async saveVariable(roomUrl: string, key: string, value: string): Promise { - // The value is passed to JSON.stringify client side. If value is "undefined", JSON.stringify returns "undefined" - // which is translated to empty string when fetching the value in the pusher. - // Therefore, empty string server side == undefined client side. - if (value === "") { - return this.hdel(roomUrl, key); - } - - // TODO: SLOW WRITING EVERY 2 SECONDS WITH A TIMEOUT - - // @ts-ignore See https://stackoverflow.com/questions/63539317/how-do-i-use-hmset-with-node-promisify - return this.hset(roomUrl, key, value); - } -} diff --git a/back/src/Services/Repository/VariablesRepository.ts b/back/src/Services/Repository/VariablesRepository.ts deleted file mode 100644 index 9f668bcf..00000000 --- a/back/src/Services/Repository/VariablesRepository.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RedisVariablesRepository } from "./RedisVariablesRepository"; -import { redisClient } from "../RedisClient"; -import { VoidVariablesRepository } from "./VoidVariablesRepository"; -import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface"; - -let variablesRepository: VariablesRepositoryInterface; -if (!redisClient) { - console.warn("WARNING: Redis isnot configured. No variables will be persisted."); - variablesRepository = new VoidVariablesRepository(); -} else { - variablesRepository = new RedisVariablesRepository(redisClient); -} - -export { variablesRepository }; diff --git a/back/src/Services/Repository/VariablesRepositoryInterface.ts b/back/src/Services/Repository/VariablesRepositoryInterface.ts deleted file mode 100644 index d927f5ff..00000000 --- a/back/src/Services/Repository/VariablesRepositoryInterface.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface VariablesRepositoryInterface { - /** - * Load all variables for a room. - * - * Note: in Redis, variables are stored in a hashmap and the key is the roomUrl - */ - loadVariables(roomUrl: string): Promise<{ [key: string]: string }>; - - saveVariable(roomUrl: string, key: string, value: string): Promise; -} diff --git a/back/src/Services/Repository/VoidVariablesRepository.ts b/back/src/Services/Repository/VoidVariablesRepository.ts deleted file mode 100644 index 0a2664e8..00000000 --- a/back/src/Services/Repository/VoidVariablesRepository.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface"; - -/** - * Mock class in charge of NOT saving/loading variables from the data store - */ -export class VoidVariablesRepository implements VariablesRepositoryInterface { - loadVariables(roomUrl: string): Promise<{ [key: string]: string }> { - return Promise.resolve({}); - } - - saveVariable(roomUrl: string, key: string, value: string): Promise { - return Promise.resolve(0); - } -} diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 9233811b..4bd26778 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -1,82 +1,63 @@ -import { GameRoom } from "../Model/GameRoom"; +import {GameRoom} from "../Model/GameRoom"; +import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; import { + GroupDeleteMessage, + GroupUpdateMessage, ItemEventMessage, ItemStateMessage, PlayGlobalMessage, PointMessage, + PositionMessage, RoomJoinedMessage, ServerToClientMessage, + SetPlayerDetailsMessage, SilentMessage, SubMessage, + ReportPlayerMessage, + UserJoinedMessage, UserLeftMessage, UserMovedMessage, UserMovesMessage, - WebRtcDisconnectMessage, + ViewportMessage, WebRtcDisconnectMessage, WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, WebRtcStartMessage, QueryJitsiJwtMessage, SendJitsiJwtMessage, - SendUserMessage, - JoinRoomMessage, - Zone as ProtoZone, - BatchToPusherMessage, - SubToPusherMessage, - UserJoinedZoneMessage, - GroupUpdateZoneMessage, - GroupLeftZoneMessage, - WorldFullWarningMessage, - UserLeftZoneMessage, - EmoteEventMessage, - BanUserMessage, - RefreshRoomMessage, - EmotePromptMessage, - FollowRequestMessage, - FollowConfirmationMessage, - FollowAbortMessage, - VariableMessage, - BatchToPusherRoomMessage, - SubToPusherRoomMessage, - SetPlayerDetailsMessage, - PlayerDetailsUpdatedMessage, + SendUserMessage } from "../Messages/generated/messages_pb"; -import { User, UserSocket } from "../Model/User"; -import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; -import { Group } from "../Model/Group"; -import { cpuTracker } from "./CpuTracker"; -import { - GROUP_RADIUS, - JITSI_ISS, - MINIMUM_DISTANCE, - SECRET_JITSI_KEY, - TURN_STATIC_AUTH_SECRET, -} from "../Enum/EnvironmentVariable"; -import { Movable } from "../Model/Movable"; -import { PositionInterface } from "../Model/PositionInterface"; +import {PointInterface} from "../Model/Websocket/PointInterface"; +import {User} from "../Model/User"; +import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; +import {Group} from "../Model/Group"; +import {cpuTracker} from "./CpuTracker"; +import {isSetPlayerDetailsMessage} from "../Model/Websocket/SetPlayerDetailsMessage"; +import {GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; +import {Movable} from "../Model/Movable"; +import {PositionInterface} from "../Model/PositionInterface"; +import {adminApi, CharacterTexture} from "./AdminApi"; +import Direction = PositionMessage.Direction; +import {emitError, emitInBatch} from "./IoSocketHelpers"; import Jwt from "jsonwebtoken"; -import { JITSI_URL } from "../Enum/EnvironmentVariable"; -import { clientEventsEmitter } from "./ClientEventsEmitter"; -import { gaugeManager } from "./GaugeManager"; -import { RoomSocket, ZoneSocket } from "../RoomManager"; -import { Zone } from "_Model/Zone"; -import Debug from "debug"; -import { Admin } from "_Model/Admin"; -import crypto from "crypto"; +import {JITSI_URL} from "../Enum/EnvironmentVariable"; +import {clientEventsEmitter} from "./ClientEventsEmitter"; +import {gaugeManager} from "./GaugeManager"; -const debug = Debug("sockermanager"); +interface AdminSocketRoomsList { + [index: string]: number; +} +interface AdminSocketUsersList { + [index: string]: boolean; +} -function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void { - // TODO: should we batch those every 100ms? - const batchMessage = new BatchToPusherMessage(); - batchMessage.addPayload(subMessage); - - socket.write(batchMessage); +export interface AdminSocketData { + rooms: AdminSocketRoomsList, + users: AdminSocketUsersList, } export class SocketManager { - //private rooms = new Map(); - // List of rooms in process of loading. - private roomsPromises = new Map>(); - + private Worlds: Map = new Map(); + private sockets: Map = new Map(); + constructor() { clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { gaugeManager.incNbClientPerRoomGauge(roomId); @@ -86,359 +67,449 @@ export class SocketManager { }); } - public async handleJoinRoom( - socket: UserSocket, - joinRoomMessage: JoinRoomMessage - ): Promise<{ room: GameRoom; user: User }> { - //join new previous room - const { room, user } = await this.joinRoom(socket, joinRoomMessage); - - if (!socket.writable) { - console.warn("Socket was aborted"); - return { - room, - user, - }; + getAdminSocketDataFor(roomId:string): AdminSocketData { + const data:AdminSocketData = { + rooms: {}, + users: {}, } - const roomJoinedMessage = new RoomJoinedMessage(); - roomJoinedMessage.setTagList(joinRoomMessage.getTagList()); - roomJoinedMessage.setUserroomtoken(joinRoomMessage.getUserroomtoken()); - - for (const [itemId, item] of room.getItemsState().entries()) { - const itemStateMessage = new ItemStateMessage(); - itemStateMessage.setItemid(itemId); - itemStateMessage.setStatejson(JSON.stringify(item)); - - roomJoinedMessage.addItem(itemStateMessage); + const room = this.Worlds.get(roomId); + if (room === undefined) { + return data; } - - const variables = await room.getVariablesForTags(user.tags); - - for (const [name, value] of variables.entries()) { - const variableMessage = new VariableMessage(); - variableMessage.setName(name); - variableMessage.setValue(value); - - roomJoinedMessage.addVariable(variableMessage); - } - - roomJoinedMessage.setCurrentuserid(user.id); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); - socket.write(serverToClientMessage); - - return { - room, - user, - }; + const users = room.getUsers(); + data.rooms[roomId] = users.size; + users.forEach(user => { + data.users[user.uuid] = true + }) + return data; } - handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) { - const userMoves = userMovesMessage.toObject(); - const position = userMovesMessage.getPosition(); + handleJoinRoom(client: ExSocketInterface): void { + const position = client.position; + const viewport = client.viewport; + try { + this.sockets.set(client.userId, client); //todo: should this be at the end of the function? + //join new previous room + const gameRoom = this.joinRoom(client, position); - // If CPU is high, let's drop messages of users moving (we will only dispatch the final position) - if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) { + const things = gameRoom.setViewport(client, viewport); + + const roomJoinedMessage = new RoomJoinedMessage(); + + for (const thing of things) { + if (thing instanceof User) { + const player: ExSocketInterface|undefined = this.sockets.get(thing.id); + if (player === undefined) { + console.warn('Something went wrong. The World contains a user "'+thing.id+"' but this user does not exist in the sockets list!"); + continue; + } + + const userJoinedMessage = new UserJoinedMessage(); + userJoinedMessage.setUserid(thing.id); + userJoinedMessage.setName(player.name); + userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(player.characterLayers)); + userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(player.position)); + + roomJoinedMessage.addUser(userJoinedMessage); + roomJoinedMessage.setTagList(client.tags); + } else if (thing instanceof Group) { + const groupUpdateMessage = new GroupUpdateMessage(); + groupUpdateMessage.setGroupid(thing.getId()); + groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition())); + + roomJoinedMessage.addGroup(groupUpdateMessage); + } else { + console.error("Unexpected type for Movable returned by setViewport"); + } + } + + for (const [itemId, item] of gameRoom.getItemsState().entries()) { + const itemStateMessage = new ItemStateMessage(); + itemStateMessage.setItemid(itemId); + itemStateMessage.setStatejson(JSON.stringify(item)); + + roomJoinedMessage.addItem(itemStateMessage); + } + + roomJoinedMessage.setCurrentuserid(client.userId); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); + + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); + } + } catch (e) { + console.error('An error occurred on "join_room" event'); + console.error(e); + } + } + + handleViewport(client: ExSocketInterface, viewportMessage: ViewportMessage) { + try { + const viewport = viewportMessage.toObject(); + + client.viewport = viewport; + + const world = this.Worlds.get(client.roomId); + if (!world) { + console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'"); + return; + } + world.setViewport(client, client.viewport); + } catch (e) { + console.error('An error occurred on "SET_VIEWPORT" event'); + console.error(e); + } + } + + handleUserMovesMessage(client: ExSocketInterface, userMovesMessage: UserMovesMessage) { + try { + const userMoves = userMovesMessage.toObject(); + + // If CPU is high, let's drop messages of users moving (we will only dispatch the final position) + if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) { + return; + } + + const position = userMoves.position; + if (position === undefined) { + throw new Error('Position not found in message'); + } + const viewport = userMoves.viewport; + if (viewport === undefined) { + throw new Error('Viewport not found in message'); + } + + let direction: string; + switch (position.direction) { + case Direction.UP: + direction = 'up'; + break; + case Direction.DOWN: + direction = 'down'; + break; + case Direction.LEFT: + direction = 'left'; + break; + case Direction.RIGHT: + direction = 'right'; + break; + default: + throw new Error("Unexpected direction"); + } + + // sending to all clients in room except sender + client.position = { + x: position.x, + y: position.y, + direction, + moving: position.moving, + }; + client.viewport = viewport; + + // update position in the world + const world = this.Worlds.get(client.roomId); + if (!world) { + console.error("In USER_POSITION, could not find world with id '", client.roomId, "'"); + return; + } + world.updatePosition(client, client.position); + world.setViewport(client, client.viewport); + } catch (e) { + console.error('An error occurred on "user_position" event'); + console.error(e); + } + } + + // Useless now, will be useful again if we allow editing details in game + handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) { + const playerDetails = { + name: playerDetailsMessage.getName(), + characterLayers: playerDetailsMessage.getCharacterlayersList() + }; + //console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails); + if (!isSetPlayerDetailsMessage(playerDetails)) { + emitError(client, 'Invalid SET_PLAYER_DETAILS message received: '); return; } + client.name = playerDetails.name; + client.characterLayers = SocketManager.mergeCharacterLayersAndCustomTextures(playerDetails.characterLayers, client.textures); + } - if (position === undefined) { - throw new Error("Position not found in message"); + handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) { + try { + // update position in the world + const world = this.Worlds.get(client.roomId); + if (!world) { + console.error("In handleSilentMessage, could not find world with id '", client.roomId, "'"); + return; + } + world.setSilent(client, silentMessage.getSilent()); + } catch (e) { + console.error('An error occurred on "handleSilentMessage"'); + console.error(e); } - const viewport = userMoves.viewport; - if (viewport === undefined) { - throw new Error("Viewport not found in message"); - } - - // update position in the world - room.updatePosition(user, ProtobufUtils.toPointInterface(position)); - //room.setViewport(client, client.viewport); } - handleSetPlayerDetails(room: GameRoom, user: User, playerDetailsMessage: SetPlayerDetailsMessage) { - room.updatePlayerDetails(user, playerDetailsMessage); - } - - handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) { - room.setSilent(user, silentMessage.getSilent()); - } - - handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) { + handleItemEvent(ws: ExSocketInterface, itemEventMessage: ItemEventMessage) { const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage); - const subMessage = new SubMessage(); - subMessage.setItemeventmessage(itemEventMessage); + try { + const world = this.Worlds.get(ws.roomId); + if (!world) { + console.error("Could not find world with id '", ws.roomId, "'"); + return; + } - // Let's send the event without using the SocketIO room. - // TODO: move this in the GameRoom class. - for (const user of room.getUsers().values()) { - user.emitInBatch(subMessage); + const subMessage = new SubMessage(); + subMessage.setItemeventmessage(itemEventMessage); + + // Let's send the event without using the SocketIO room. + for (const user of world.getUsers().values()) { + const client = this.searchClientByIdOrFail(user.id); + //client.emit(SocketIoEvent.ITEM_EVENT, itemEvent); + emitInBatch(client, subMessage); + } + + world.setItemState(itemEvent.itemId, itemEvent.state); + } catch (e) { + console.error('An error occurred on "item_event"'); + console.error(e); } - - room.setItemState(itemEvent.itemId, itemEvent.state); } - handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage): Promise { - return room.setVariable(variableMessage.getName(), variableMessage.getValue(), user); + async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { + try { + const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); + if (!reportedSocket) { + throw 'reported socket user not found'; + } + //TODO report user on admin application + await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid) + } catch (e) { + console.error('An error occurred on "handleReportMessage"'); + console.error(e); + } } - emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void { + emitVideo(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void { //send only at user - const remoteUser = room.getUsers().get(data.getReceiverid()); - if (remoteUser === undefined) { - console.warn( - "While exchanging a WebRTC signal: client with id ", - data.getReceiverid(), - " does not exist. This might be a race condition." - ); + const client = this.sockets.get(data.getReceiverid()); + if (client === undefined) { + console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); return; } const webrtcSignalToClient = new WebRtcSignalToClientMessage(); - webrtcSignalToClient.setUserid(user.id); + webrtcSignalToClient.setUserid(socket.userId); webrtcSignalToClient.setSignal(data.getSignal()); - // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== "") { - const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET); - webrtcSignalToClient.setWebrtcusername(username); - webrtcSignalToClient.setWebrtcpassword(password); - } const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); - //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); - //} + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); + } } - emitScreenSharing(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void { + emitScreenSharing(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void { //send only at user - const remoteUser = room.getUsers().get(data.getReceiverid()); - if (remoteUser === undefined) { - console.warn( - "While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", - data.getReceiverid(), - " does not exist. This might be a race condition." - ); + const client = this.sockets.get(data.getReceiverid()); + if (client === undefined) { + console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); return; } const webrtcSignalToClient = new WebRtcSignalToClientMessage(); - webrtcSignalToClient.setUserid(user.id); + webrtcSignalToClient.setUserid(socket.userId); webrtcSignalToClient.setSignal(data.getSignal()); - // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== "") { - const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET); - webrtcSignalToClient.setWebrtcusername(username); - webrtcSignalToClient.setWebrtcpassword(password); - } const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); - //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); - //} + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); + } } - leaveRoom(room: GameRoom, user: User) { + private searchClientByIdOrFail(userId: number): ExSocketInterface { + const client: ExSocketInterface|undefined = this.sockets.get(userId); + if (client === undefined) { + throw new Error("Could not find user with id " + userId); + } + return client; + } + + leaveRoom(Client : ExSocketInterface){ // leave previous room and world - try { - //user leave previous world - room.leave(user); - if (room.isEmpty()) { - this.roomsPromises.delete(room.roomUrl); - gaugeManager.decNbRoomGauge(); - debug('Room is empty. Deleting room "%s"', room.roomUrl); + if(Client.roomId){ + try { + //user leave previous world + const world: GameRoom | undefined = this.Worlds.get(Client.roomId); + if (world) { + world.leave(Client); + if (world.isEmpty()) { + this.Worlds.delete(Client.roomId); + } + } + //user leave previous room + //Client.leave(Client.roomId); + } finally { + //delete Client.roomId; + this.sockets.delete(Client.userId); + clientEventsEmitter.emitClientLeave(Client.userUuid, Client.roomId); + console.log('A user left (', this.sockets.size, ' connected users)'); } - } finally { - clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl); - console.log("A user left"); } } async getOrCreateRoom(roomId: string): Promise { - //check and create new room - let roomPromise = this.roomsPromises.get(roomId); - if (roomPromise === undefined) { - roomPromise = GameRoom.create( + //check and create new world for a room + let world = this.Worlds.get(roomId) + if(world === undefined){ + world = new GameRoom( roomId, (user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.disConnectedUser(user, group), MINIMUM_DISTANCE, GROUP_RADIUS, - (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => - this.onZoneEnter(thing, fromZone, listener), - (thing: Movable, position: PositionInterface, listener: ZoneSocket) => - this.onClientMove(thing, position, listener), - (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => - this.onClientLeave(thing, newZone, listener), - (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => - this.onEmote(emoteEventMessage, listener), - (playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, listener: ZoneSocket) => - this.onPlayerDetailsUpdated(playerDetailsUpdatedMessage, listener) - ) - .then((gameRoom) => { - gaugeManager.incNbRoomGauge(); - return gameRoom; - }) - .catch((e) => { - this.roomsPromises.delete(roomId); - throw e; - }); - this.roomsPromises.set(roomId, roomPromise); + (thing: Movable, listener: User) => this.onRoomEnter(thing, listener), + (thing: Movable, position:PositionInterface, listener:User) => this.onClientMove(thing, position, listener), + (thing: Movable, listener:User) => this.onClientLeave(thing, listener) + ); + if (!world.anonymous) { + const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) + world.tags = data.tags + world.policyType = Number(data.policy_type) + } + this.Worlds.set(roomId, world); } - return roomPromise; + return Promise.resolve(world) } - private async joinRoom( - socket: UserSocket, - joinRoomMessage: JoinRoomMessage - ): Promise<{ room: GameRoom; user: User }> { - const roomId = joinRoomMessage.getRoomid(); + private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom { - const room = await socketManager.getOrCreateRoom(roomId); + const roomId = client.roomId; + client.position = position; + const world = this.Worlds.get(roomId) + if(world === undefined){ + throw new Error('Could not find room for ID: '+client.roomId) + } + + // Dispatch groups position to newly connected user + world.getGroups().forEach((group: Group) => { + this.emitCreateUpdateGroupEvent(client, group); + }); //join world - const user = room.join(socket, joinRoomMessage); - - clientEventsEmitter.emitClientJoin(user.uuid, roomId); - console.log(new Date().toISOString() + " A user joined"); - return { room, user }; + world.join(client, client.position); + clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); + console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); + return world; } - private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) { + private onRoomEnter(thing: Movable, listener: User) { + const clientListener = this.searchClientByIdOrFail(listener.id); if (thing instanceof User) { - const userJoinedZoneMessage = new UserJoinedZoneMessage(); - if (!Number.isInteger(thing.id)) { - throw new Error(`clientUser.userId is not an integer ${thing.id}`); - } - userJoinedZoneMessage.setUserid(thing.id); - userJoinedZoneMessage.setUseruuid(thing.uuid); - userJoinedZoneMessage.setName(thing.name); - userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); - userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); - userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone)); - if (thing.visitCardUrl) { - userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl); - } - userJoinedZoneMessage.setCompanion(thing.companion); - if (thing.outlineColor === undefined) { - userJoinedZoneMessage.setHasoutline(false); - } else { - userJoinedZoneMessage.setHasoutline(true); - userJoinedZoneMessage.setOutlinecolor(thing.outlineColor); - } + const clientUser = this.searchClientByIdOrFail(thing.id); - const subMessage = new SubToPusherMessage(); - subMessage.setUserjoinedzonemessage(userJoinedZoneMessage); + const userJoinedMessage = new UserJoinedMessage(); + if (!Number.isInteger(clientUser.userId)) { + throw new Error('clientUser.userId is not an integer '+clientUser.userId); + } + userJoinedMessage.setUserid(clientUser.userId); + userJoinedMessage.setName(clientUser.name); + userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(clientUser.characterLayers)); + userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); - emitZoneMessage(subMessage, listener); - //listener.emitInBatch(subMessage); + const subMessage = new SubMessage(); + subMessage.setUserjoinedmessage(userJoinedMessage); + + emitInBatch(clientListener, subMessage); } else if (thing instanceof Group) { - this.emitCreateUpdateGroupEvent(listener, fromZone, thing); + this.emitCreateUpdateGroupEvent(clientListener, thing); } else { - console.error("Unexpected type for Movable."); + console.error('Unexpected type for Movable.'); } } - private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void { + private onClientMove(thing: Movable, position:PositionInterface, listener:User): void { + const clientListener = this.searchClientByIdOrFail(listener.id); if (thing instanceof User) { - const userMovedMessage = new UserMovedMessage(); - userMovedMessage.setUserid(thing.id); - userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); + const clientUser = this.searchClientByIdOrFail(thing.id); - const subMessage = new SubToPusherMessage(); + const userMovedMessage = new UserMovedMessage(); + userMovedMessage.setUserid(clientUser.userId); + userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); + + const subMessage = new SubMessage(); subMessage.setUsermovedmessage(userMovedMessage); - emitZoneMessage(subMessage, listener); - //listener.emitInBatch(subMessage); + clientListener.emitInBatch(subMessage); //console.log("Sending USER_MOVED event"); } else if (thing instanceof Group) { - this.emitCreateUpdateGroupEvent(listener, null, thing); + this.emitCreateUpdateGroupEvent(clientListener, thing); } else { - console.error("Unexpected type for Movable."); + console.error('Unexpected type for Movable.'); } } - private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) { + private onClientLeave(thing: Movable, listener:User) { + const clientListener = this.searchClientByIdOrFail(listener.id); if (thing instanceof User) { - this.emitUserLeftEvent(listener, thing.id, newZone); + const clientUser = this.searchClientByIdOrFail(thing.id); + this.emitUserLeftEvent(clientListener, clientUser.userId); } else if (thing instanceof Group) { - this.emitDeleteGroupEvent(listener, thing.getId(), newZone); + this.emitDeleteGroupEvent(clientListener, thing.getId()); } else { - console.error("Unexpected type for Movable."); + console.error('Unexpected type for Movable.'); } } - private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) { - const subMessage = new SubToPusherMessage(); - subMessage.setEmoteeventmessage(emoteEventMessage); - - emitZoneMessage(subMessage, client); - } - - private onPlayerDetailsUpdated(playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage, client: ZoneSocket) { - const subMessage = new SubToPusherMessage(); - subMessage.setPlayerdetailsupdatedmessage(playerDetailsUpdatedMessage); - - emitZoneMessage(subMessage, client); - } - - private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void { + private emitCreateUpdateGroupEvent(client: ExSocketInterface, group: Group): void { const position = group.getPosition(); const pointMessage = new PointMessage(); pointMessage.setX(Math.floor(position.x)); pointMessage.setY(Math.floor(position.y)); - const groupUpdateMessage = new GroupUpdateZoneMessage(); + const groupUpdateMessage = new GroupUpdateMessage(); groupUpdateMessage.setGroupid(group.getId()); groupUpdateMessage.setPosition(pointMessage); groupUpdateMessage.setGroupsize(group.getSize); - groupUpdateMessage.setFromzone(this.toProtoZone(fromZone)); - const subMessage = new SubToPusherMessage(); - subMessage.setGroupupdatezonemessage(groupUpdateMessage); + const subMessage = new SubMessage(); + subMessage.setGroupupdatemessage(groupUpdateMessage); - emitZoneMessage(subMessage, client); - //client.emitInBatch(subMessage); + emitInBatch(client, subMessage); + //socket.emit(SocketIoEvent.GROUP_CREATE_UPDATE, groupUpdateMessage.serializeBinary().buffer); } - private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void { - const groupDeleteMessage = new GroupLeftZoneMessage(); + private emitDeleteGroupEvent(client: ExSocketInterface, groupId: number): void { + const groupDeleteMessage = new GroupDeleteMessage(); groupDeleteMessage.setGroupid(groupId); - groupDeleteMessage.setTozone(this.toProtoZone(newZone)); - const subMessage = new SubToPusherMessage(); - subMessage.setGroupleftzonemessage(groupDeleteMessage); + const subMessage = new SubMessage(); + subMessage.setGroupdeletemessage(groupDeleteMessage); - emitZoneMessage(subMessage, client); - //user.emitInBatch(subMessage); + emitInBatch(client, subMessage); } - private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void { - const userLeftMessage = new UserLeftZoneMessage(); + private emitUserLeftEvent(client: ExSocketInterface, userId: number): void { + const userLeftMessage = new UserLeftMessage(); userLeftMessage.setUserid(userId); - userLeftMessage.setTozone(this.toProtoZone(newZone)); - const subMessage = new SubToPusherMessage(); - subMessage.setUserleftzonemessage(userLeftMessage); + const subMessage = new SubMessage(); + subMessage.setUserleftmessage(userLeftMessage); - emitZoneMessage(subMessage, client); - } - - private toProtoZone(zone: Zone | null): ProtoZone | undefined { - if (zone !== null) { - const zoneMessage = new ProtoZone(); - zoneMessage.setX(zone.x); - zoneMessage.setY(zone.y); - return zoneMessage; - } - return undefined; + emitInBatch(client, subMessage); } private joinWebRtcRoom(user: User, group: Group) { + /*const roomId: string = "webrtcroom"+group.getId(); + if (user.socket.webRtcRoomId === roomId) { + return; + }*/ + for (const otherUser of group.getUsers()) { if (user === otherUser) { continue; @@ -447,54 +518,31 @@ export class SocketManager { // Let's send 2 messages: one to the user joining the group and one to the other user const webrtcStartMessage1 = new WebRtcStartMessage(); webrtcStartMessage1.setUserid(otherUser.id); + webrtcStartMessage1.setName(otherUser.socket.name); webrtcStartMessage1.setInitiator(true); - if (TURN_STATIC_AUTH_SECRET !== "") { - const { username, password } = this.getTURNCredentials( - otherUser.id.toString(), - TURN_STATIC_AUTH_SECRET - ); - webrtcStartMessage1.setWebrtcusername(username); - webrtcStartMessage1.setWebrtcpassword(password); - } const serverToClientMessage1 = new ServerToClientMessage(); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); - user.socket.write(serverToClientMessage1); + if (!user.socket.disconnecting) { + user.socket.send(serverToClientMessage1.serializeBinary().buffer, true); + //console.log('Sending webrtcstart initiator to '+user.socket.userId) + } const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); + webrtcStartMessage2.setName(user.socket.name); webrtcStartMessage2.setInitiator(false); - if (TURN_STATIC_AUTH_SECRET !== "") { - const { username, password } = this.getTURNCredentials(user.id.toString(), TURN_STATIC_AUTH_SECRET); - webrtcStartMessage2.setWebrtcusername(username); - webrtcStartMessage2.setWebrtcpassword(password); - } const serverToClientMessage2 = new ServerToClientMessage(); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); - otherUser.socket.write(serverToClientMessage2); - } - } + if (!otherUser.socket.disconnecting) { + otherUser.socket.send(serverToClientMessage2.serializeBinary().buffer, true); + //console.log('Sending webrtcstart to '+otherUser.socket.userId) + } - /** - * Computes a unique user/password for the TURN server, using a shared secret between the WorkAdventure API server - * and the Coturn server. - * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey` - */ - private getTURNCredentials(name: string, secret: string): { username: string; password: string } { - const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours - const username = [unixTimeStamp, name].join(":"); - const hmac = crypto.createHmac("sha1", secret); - hmac.setEncoding("base64"); - hmac.write(username); - hmac.end(); - const password = hmac.read() as string; - return { - username: username, - password: password, - }; + } } //disconnect user @@ -515,9 +563,10 @@ export class SocketManager { const serverToClientMessage1 = new ServerToClientMessage(); serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); - //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage1); - //} + if (!otherUser.socket.disconnecting) { + otherUser.socket.send(serverToClientMessage1.serializeBinary().buffer, true); + } + const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); webrtcDisconnectMessage2.setUserid(otherUser.id); @@ -525,45 +574,77 @@ export class SocketManager { const serverToClientMessage2 = new ServerToClientMessage(); serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); - //if (!user.socket.disconnecting) { - user.socket.write(serverToClientMessage2); - //} + if (!user.socket.disconnecting) { + user.socket.send(serverToClientMessage2.serializeBinary().buffer, true); + } } } - public getWorlds(): Map> { - return this.roomsPromises; + emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) { + try { + const world = this.Worlds.get(client.roomId); + if (!world) { + console.error("In emitPlayGlobalMessage, could not find world with id '", client.roomId, "'"); + return; + } + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setPlayglobalmessage(playglobalmessage); + + for (const [id, user] of world.getUsers().entries()) { + user.socket.send(serverToClientMessage.serializeBinary().buffer, true); + } + } catch (e) { + console.error('An error occurred on "emitPlayGlobalMessage" event'); + console.error(e); + } + } - public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) { + public getWorlds(): Map { + return this.Worlds; + } + + /** + * + * @param token + */ + searchClientByUuid(uuid: string): ExSocketInterface | null { + for(const socket of this.sockets.values()){ + if(socket.userUuid === uuid){ + return socket; + } + } + return null; + } + + + public handleQueryJitsiJwtMessage(client: ExSocketInterface, queryJitsiJwtMessage: QueryJitsiJwtMessage) { const room = queryJitsiJwtMessage.getJitsiroom(); const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead. - if (SECRET_JITSI_KEY === "") { - throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi."); + if (SECRET_JITSI_KEY === '') { + throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); } // Let's see if the current client has - const isAdmin = user.tags.includes(tag); + const isAdmin = client.tags.includes(tag); - const jwt = Jwt.sign( - { - aud: "jitsi", - iss: JITSI_ISS, - sub: JITSI_URL, - room: room, - moderator: isAdmin, - }, - SECRET_JITSI_KEY, - { - expiresIn: "1d", - algorithm: "HS256", - header: { - alg: "HS256", - typ: "JWT", - }, - } - ); + const jwt = Jwt.sign({ + "aud": "jitsi", + "iss": JITSI_ISS, + "sub": JITSI_URL, + "room": room, + "moderator": isAdmin + }, SECRET_JITSI_KEY, { + expiresIn: '1d', + algorithm: "HS256", + header: + { + "alg": "HS256", + "typ": "JWT" + } + }); const sendJitsiJwtMessage = new SendJitsiJwtMessage(); sendJitsiJwtMessage.setJitsiroom(room); @@ -572,302 +653,53 @@ export class SocketManager { const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setSendjitsijwtmessage(sendJitsiJwtMessage); - user.socket.write(serverToClientMessage); + client.send(serverToClientMessage.serializeBinary().buffer, true); } - public handleSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) { + public emitSendUserMessage(messageToSend: {userUuid: string, message: string, type: string}): ExSocketInterface { + const socket = this.searchClientByUuid(messageToSend.userUuid); + if(!socket){ + throw 'socket was not found'; + } + const sendUserMessage = new SendUserMessage(); - sendUserMessage.setMessage(sendUserMessageToSend.getMessage()); - sendUserMessage.setType(sendUserMessageToSend.getType()); + sendUserMessage.setMessage(messageToSend.message); + sendUserMessage.setType(messageToSend.type); const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setSendusermessage(sendUserMessage); - user.socket.write(serverToClientMessage); - } - public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) { - const banUserMessage = new BanUserMessage(); - banUserMessage.setMessage(banUserMessageToSend.getMessage()); - banUserMessage.setType(banUserMessageToSend.getType()); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setSendusermessage(banUserMessage); - user.socket.write(serverToClientMessage); - - setTimeout(() => { - // Let's leave the room now. - room.leave(user); - // Let's close the connection when the user is banned. - user.socket.end(); - }, 10000); - } - - public async addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise { - const room = await this.roomsPromises.get(roomId); - if (!room) { - throw new Error("In addZoneListener, could not find room with id '" + roomId + "'"); + if (!socket.disconnecting) { + socket.send(serverToClientMessage.serializeBinary().buffer, true); } + return socket; + } - const things = room.addZoneListener(call, x, y); - - const batchMessage = new BatchToPusherMessage(); - - for (const thing of things) { - if (thing instanceof User) { - const userJoinedMessage = new UserJoinedZoneMessage(); - userJoinedMessage.setUserid(thing.id); - userJoinedMessage.setUseruuid(thing.uuid); - userJoinedMessage.setName(thing.name); - userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); - userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); - if (thing.visitCardUrl) { - userJoinedMessage.setVisitcardurl(thing.visitCardUrl); + /** + * Merges the characterLayers received from the front (as an array of string) with the custom textures from the back. + */ + static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] { + const characterLayerObjs: CharacterLayer[] = []; + for (const characterLayer of characterLayers) { + if (characterLayer.startsWith('customCharacterTexture')) { + const customCharacterLayerId: number = +characterLayer.substr(22); + for (const memberTexture of memberTextures) { + if (memberTexture.id == customCharacterLayerId) { + characterLayerObjs.push({ + name: characterLayer, + url: memberTexture.url + }) + break; + } } - userJoinedMessage.setCompanion(thing.companion); - - const subMessage = new SubToPusherMessage(); - subMessage.setUserjoinedzonemessage(userJoinedMessage); - - batchMessage.addPayload(subMessage); - } else if (thing instanceof Group) { - const groupUpdateMessage = new GroupUpdateZoneMessage(); - groupUpdateMessage.setGroupid(thing.getId()); - groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition())); - - const subMessage = new SubToPusherMessage(); - subMessage.setGroupupdatezonemessage(groupUpdateMessage); - - batchMessage.addPayload(subMessage); } else { - console.error("Unexpected type for Movable returned by setViewport"); + characterLayerObjs.push({ + name: characterLayer, + url: undefined + }) } } - - call.write(batchMessage); - } - - async removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise { - const room = await this.roomsPromises.get(roomId); - if (!room) { - throw new Error("In removeZoneListener, could not find room with id '" + roomId + "'"); - } - - room.removeZoneListener(call, x, y); - } - - async addRoomListener(call: RoomSocket, roomId: string) { - const room = await this.getOrCreateRoom(roomId); - if (!room) { - throw new Error("In addRoomListener, could not find room with id '" + roomId + "'"); - } - - room.addRoomListener(call); - - const batchMessage = new BatchToPusherRoomMessage(); - - call.write(batchMessage); - } - - async removeRoomListener(call: RoomSocket, roomId: string) { - const room = await this.roomsPromises.get(roomId); - if (!room) { - throw new Error("In removeRoomListener, could not find room with id '" + roomId + "'"); - } - - room.removeRoomListener(call); - } - - public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise { - const room = await socketManager.getOrCreateRoom(roomId); - - room.adminJoin(admin); - - return room; - } - - public leaveAdminRoom(room: GameRoom, admin: Admin) { - room.adminLeave(admin); - if (room.isEmpty()) { - this.roomsPromises.delete(room.roomUrl); - gaugeManager.decNbRoomGauge(); - debug('Room is empty. Deleting room "%s"', room.roomUrl); - } - } - - public async sendAdminMessage(roomId: string, recipientUuid: string, message: string, type: string): Promise { - const room = await this.roomsPromises.get(roomId); - if (!room) { - console.error( - "In sendAdminMessage, could not find room with id '" + - roomId + - "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" - ); - return; - } - - const recipients = room.getUsersByUuid(recipientUuid); - if (recipients.length === 0) { - console.error( - "In sendAdminMessage, could not find user with id '" + - recipientUuid + - "'. Maybe the user left the room a few milliseconds ago and there was a race condition?" - ); - return; - } - - for (const recipient of recipients) { - const sendUserMessage = new SendUserMessage(); - sendUserMessage.setMessage(message); - sendUserMessage.setType(type); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setSendusermessage(sendUserMessage); - - recipient.socket.write(serverToClientMessage); - } - } - - public async banUser(roomId: string, recipientUuid: string, message: string): Promise { - const room = await this.roomsPromises.get(roomId); - if (!room) { - console.error( - "In banUser, could not find room with id '" + - roomId + - "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" - ); - return; - } - - const recipients = room.getUsersByUuid(recipientUuid); - if (recipients.length === 0) { - console.error( - "In banUser, could not find user with id '" + - recipientUuid + - "'. Maybe the user left the room a few milliseconds ago and there was a race condition?" - ); - return; - } - - for (const recipient of recipients) { - // Let's leave the room now. - room.leave(recipient); - - const banUserMessage = new BanUserMessage(); - banUserMessage.setMessage(message); - banUserMessage.setType("banned"); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setBanusermessage(banUserMessage); - - // Let's close the connection when the user is banned. - recipient.socket.write(serverToClientMessage); - recipient.socket.end(); - } - } - - async sendAdminRoomMessage(roomId: string, message: string, type: string) { - const room = await this.roomsPromises.get(roomId); - if (!room) { - //todo: this should cause the http call to return a 500 - console.error( - "In sendAdminRoomMessage, could not find room with id '" + - roomId + - "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" - ); - return; - } - - room.getUsers().forEach((recipient) => { - const sendUserMessage = new SendUserMessage(); - sendUserMessage.setMessage(message); - sendUserMessage.setType(type); - - const clientMessage = new ServerToClientMessage(); - clientMessage.setSendusermessage(sendUserMessage); - - recipient.socket.write(clientMessage); - }); - } - - async dispatchWorldFullWarning(roomId: string): Promise { - const room = await this.roomsPromises.get(roomId); - if (!room) { - //todo: this should cause the http call to return a 500 - console.error( - "In dispatchWorldFullWarning, could not find room with id '" + - roomId + - "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" - ); - return; - } - - room.getUsers().forEach((recipient) => { - const worldFullMessage = new WorldFullWarningMessage(); - - const clientMessage = new ServerToClientMessage(); - clientMessage.setWorldfullwarningmessage(worldFullMessage); - - recipient.socket.write(clientMessage); - }); - } - - async dispatchRoomRefresh(roomId: string): Promise { - const room = await this.roomsPromises.get(roomId); - if (!room) { - return; - } - - const versionNumber = room.incrementVersion(); - room.getUsers().forEach((recipient) => { - const worldFullMessage = new RefreshRoomMessage(); - worldFullMessage.setRoomid(roomId); - worldFullMessage.setVersionnumber(versionNumber); - - const clientMessage = new ServerToClientMessage(); - clientMessage.setRefreshroommessage(worldFullMessage); - - recipient.socket.write(clientMessage); - }); - } - - handleEmoteEventMessage(room: GameRoom, user: User, emotePromptMessage: EmotePromptMessage) { - const emoteEventMessage = new EmoteEventMessage(); - emoteEventMessage.setEmote(emotePromptMessage.getEmote()); - emoteEventMessage.setActoruserid(user.id); - room.emitEmoteEvent(user, emoteEventMessage); - } - - handleFollowRequestMessage(room: GameRoom, user: User, message: FollowRequestMessage) { - const clientMessage = new ServerToClientMessage(); - clientMessage.setFollowrequestmessage(message); - room.sendToOthersInGroupIncludingUser(user, clientMessage); - } - - handleFollowConfirmationMessage(room: GameRoom, user: User, message: FollowConfirmationMessage) { - const leader = room.getUserById(message.getLeader()); - if (!leader) { - const message = `Could not follow user "{message.getLeader()}" in room "{room.roomUrl}".`; - console.info(message, "Maybe the user just left."); - return; - } - - // By security, we look at the group leader. If the group leader is NOT the leader in the message, - // everybody should stop following the group leader (to avoid having 2 group leaders) - if (user?.group?.leader && user?.group?.leader !== leader) { - user?.group?.leader?.stopLeading(); - } - - leader.addFollower(user); - } - - handleFollowAbortMessage(room: GameRoom, user: User, message: FollowAbortMessage) { - if (user.id === message.getLeader()) { - user?.group?.leader?.stopLeading(); - } else { - // Forward message - const leader = room.getUserById(message.getLeader()); - leader?.delFollower(user); - } + return characterLayerObjs; } } diff --git a/back/src/Services/VariableError.ts b/back/src/Services/VariableError.ts deleted file mode 100644 index 183fab2c..00000000 --- a/back/src/Services/VariableError.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Errors related to variable handling. - */ -export class VariableError extends Error { - constructor(message: string) { - super(message); - Object.setPrototypeOf(this, VariableError.prototype); - } -} diff --git a/back/src/Services/VariablesManager.ts b/back/src/Services/VariablesManager.ts deleted file mode 100644 index f7e65a66..00000000 --- a/back/src/Services/VariablesManager.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Handles variables shared between the scripting API and the server. - */ -import { ITiledMap, ITiledMapLayer, ITiledMapObject } from "@workadventure/tiled-map-type-guard/dist"; -import { User } from "_Model/User"; -import { variablesRepository } from "./Repository/VariablesRepository"; -import { redisClient } from "./RedisClient"; -import { VariableError } from "./VariableError"; - -interface Variable { - defaultValue?: string; - persist?: boolean; - readableBy?: string; - writableBy?: string; -} - -export class VariablesManager { - /** - * The actual values of the variables for the current room - */ - private _variables = new Map(); - - /** - * The list of variables that are allowed - */ - private variableObjects: Map | undefined; - - /** - * @param map The map can be "null" if it is hosted on a private network. In this case, we assume this is a test setup and bypass any server-side checks. - */ - constructor(private roomUrl: string, private map: ITiledMap | null) { - // We initialize the list of variable object at room start. The objects cannot be edited later - // (otherwise, this would cause a security issue if the scripting API can edit this list of objects) - if (map) { - this.variableObjects = VariablesManager.findVariablesInMap(map); - - // Let's initialize default values - for (const [name, variableObject] of this.variableObjects.entries()) { - if (variableObject.defaultValue !== undefined) { - this._variables.set(name, variableObject.defaultValue); - } - } - } - } - - /** - * Let's load data from the Redis backend. - */ - public async init(): Promise { - if (!this.shouldPersist()) { - return this; - } - const variables = await variablesRepository.loadVariables(this.roomUrl); - for (const key in variables) { - // Let's only set variables if they are in the map (if the map has changed, maybe stored variables do not exist anymore) - if (this.variableObjects) { - const variableObject = this.variableObjects.get(key); - if (variableObject === undefined) { - continue; - } - if (!variableObject.persist) { - continue; - } - } - - this._variables.set(key, variables[key]); - } - return this; - } - - /** - * Returns true if saving should be enabled, and false otherwise. - * - * Saving is enabled if REDIS_HOST is set - * unless we are editing a local map - * unless we are in dev mode in which case it is ok to save - * - * @private - */ - private shouldPersist(): boolean { - return redisClient !== null && (this.map !== null || process.env.NODE_ENV === "development"); - } - - private static findVariablesInMap(map: ITiledMap): Map { - const objects = new Map(); - for (const layer of map.layers) { - this.recursiveFindVariablesInLayer(layer, objects); - } - return objects; - } - - private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map): void { - if (layer.type === "objectgroup") { - for (const object of layer.objects) { - if (object.type === "variable") { - if (object.template) { - console.warn( - 'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.' - ); - continue; - } - - // We store a copy of the object (to make it immutable) - objects.set(object.name as string, this.iTiledObjectToVariable(object)); - } - } - } else if (layer.type === "group") { - for (const innerLayer of layer.layers as ITiledMapLayer[]) { - this.recursiveFindVariablesInLayer(innerLayer, objects); - } - } - } - - private static iTiledObjectToVariable(object: ITiledMapObject): Variable { - const variable: Variable = {}; - - if (object.properties) { - for (const property of object.properties) { - const value = property.value as unknown; - switch (property.name) { - case "default": - variable.defaultValue = JSON.stringify(value); - break; - case "persist": - if (typeof value !== "boolean") { - throw new Error('The persist property of variable "' + object.name + '" must be a boolean'); - } - variable.persist = value; - break; - case "writableBy": - if (typeof value !== "string") { - throw new Error( - 'The writableBy property of variable "' + object.name + '" must be a string' - ); - } - if (value) { - variable.writableBy = value; - } - break; - case "readableBy": - if (typeof value !== "string") { - throw new Error( - 'The readableBy property of variable "' + object.name + '" must be a string' - ); - } - if (value) { - variable.readableBy = value; - } - break; - } - } - } - - return variable; - } - - /** - * Sets the variable. - * - * Returns who is allowed to read the variable (the readableby property) or "undefined" if anyone can read it. - * Also, returns "false" if the variable was not modified (because we set it to the value it already has) - * - * @param name - * @param value - * @param user - */ - setVariable(name: string, value: string, user: User): string | undefined | false { - let readableBy: string | undefined; - let variableObject: Variable | undefined; - if (this.variableObjects) { - variableObject = this.variableObjects.get(name); - if (variableObject === undefined) { - throw new VariableError( - 'Trying to set a variable "' + name + '" that is not defined as an object in the map.' - ); - } - - if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) { - throw new VariableError( - 'Trying to set a variable "' + - name + - '". User "' + - user.name + - '" does not have sufficient permission. Required tag: "' + - variableObject.writableBy + - '". User tags: ' + - user.tags.join(", ") + - "." - ); - } - - readableBy = variableObject.readableBy; - } - - // If the value is not modified, return false - if (this._variables.get(name) === value) { - return false; - } - - this._variables.set(name, value); - - if (variableObject !== undefined && variableObject.persist) { - variablesRepository - .saveVariable(this.roomUrl, name, value) - .catch((e) => console.error("Error while saving variable in Redis:", e)); - } - - return readableBy; - } - - public getVariablesForTags(tags: string[]): Map { - if (this.variableObjects === undefined) { - return this._variables; - } - - const readableVariables = new Map(); - - for (const [key, value] of this._variables.entries()) { - const variableObject = this.variableObjects.get(key); - if (variableObject === undefined) { - throw new Error('Unexpected variable "' + key + '" found has no associated variableObject.'); - } - if (!variableObject.readableBy || tags.includes(variableObject.readableBy)) { - readableVariables.set(key, value); - } - } - return readableVariables; - } -} diff --git a/back/tests/GameRoomTest.ts b/back/tests/GameRoomTest.ts index d4e83daf..80926644 100644 --- a/back/tests/GameRoomTest.ts +++ b/back/tests/GameRoomTest.ts @@ -1,148 +1,97 @@ import "jasmine"; -import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom"; -import { Point } from "../src/Model/Websocket/MessageUserPosition"; +import {GameRoom, ConnectCallback, DisconnectCallback } from "../src/Model/GameRoom"; +import {Point} from "../src/Model/Websocket/MessageUserPosition"; import { Group } from "../src/Model/Group"; -import { User, UserSocket } from "_Model/User"; -import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb"; -import Direction = PositionMessage.Direction; -import { EmoteCallback } from "_Model/Zone"; +import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import {User} from "_Model/User"; -function createMockUser(userId: number): User { +function createMockUser(userId: number): ExSocketInterface { return { - userId, - } as unknown as User; + userId + } as ExSocketInterface; } -function createMockUserSocket(): UserSocket { - return {} as unknown as UserSocket; -} - -function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage { - const positionMessage = new PositionMessage(); - positionMessage.setX(x); - positionMessage.setY(y); - positionMessage.setDirection(Direction.DOWN); - positionMessage.setMoving(false); - const joinRoomMessage = new JoinRoomMessage(); - joinRoomMessage.setUseruuid("1"); - joinRoomMessage.setIpaddress("10.0.0.2"); - joinRoomMessage.setName("foo"); - joinRoomMessage.setRoomid("_/global/test.json"); - joinRoomMessage.setPositionmessage(positionMessage); - return joinRoomMessage; -} - -const emote: EmoteCallback = (emoteEventMessage, listener): void => {}; - describe("GameRoom", () => { - it("should connect user1 and user2", async () => { + it("should connect user1 and user2", () => { let connectCalledNumber: number = 0; const connect: ConnectCallback = (user: User, group: Group): void => { connectCalledNumber++; - }; - const disconnect: DisconnectCallback = (user: User, group: Group): void => {}; + } + const disconnect: DisconnectCallback = (user: User, group: Group): void => { - const world = await GameRoom.create( - "https://play.workadventu.re/_/global/localhost/test.json", - connect, - disconnect, - 160, - 160, - () => {}, - () => {}, - () => {}, - emote, - () => {} - ); + } - const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100)); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); - const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100)); + world.join(createMockUser(1), new Point(100, 100)); - world.updatePosition(user2, new Point(261, 100)); + world.join(createMockUser(2), new Point(500, 100)); + + world.updatePosition({ userId: 2 }, new Point(261, 100)); expect(connectCalledNumber).toBe(0); - world.updatePosition(user2, new Point(101, 100)); + world.updatePosition({ userId: 2 }, new Point(101, 100)); expect(connectCalledNumber).toBe(2); - world.updatePosition(user2, new Point(102, 100)); + world.updatePosition({ userId: 2 }, new Point(102, 100)); expect(connectCalledNumber).toBe(2); }); - it("should connect 3 users", async () => { + it("should connect 3 users", () => { let connectCalled: boolean = false; const connect: ConnectCallback = (user: User, group: Group): void => { connectCalled = true; - }; - const disconnect: DisconnectCallback = (user: User, group: Group): void => {}; + } + const disconnect: DisconnectCallback = (user: User, group: Group): void => { - const world = await GameRoom.create( - "https://play.workadventu.re/_/global/localhost/test.json", - connect, - disconnect, - 160, - 160, - () => {}, - () => {}, - () => {}, - emote, - () => {} - ); + } - const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100)); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); - const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 200, 100)); + world.join(createMockUser(1), new Point(100, 100)); + + world.join(createMockUser(2), new Point(200, 100)); expect(connectCalled).toBe(true); connectCalled = false; // baz joins at the outer limit of the group - const user3 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 311, 100)); + world.join(createMockUser(3), new Point(311, 100)); expect(connectCalled).toBe(false); - world.updatePosition(user3, new Point(309, 100)); + world.updatePosition({ userId: 3 }, new Point(309, 100)); expect(connectCalled).toBe(true); }); - it("should disconnect user1 and user2", async () => { + it("should disconnect user1 and user2", () => { let connectCalled: boolean = false; let disconnectCallNumber: number = 0; const connect: ConnectCallback = (user: User, group: Group): void => { connectCalled = true; - }; + } const disconnect: DisconnectCallback = (user: User, group: Group): void => { disconnectCallNumber++; - }; + } - const world = await GameRoom.create( - "https://play.workadventu.re/_/global/localhost/test.json", - connect, - disconnect, - 160, - 160, - () => {}, - () => {}, - () => {}, - emote, - () => {} - ); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); - const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100)); + world.join(createMockUser(1), new Point(100, 100)); - const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100)); + world.join(createMockUser(2), new Point(259, 100)); expect(connectCalled).toBe(true); expect(disconnectCallNumber).toBe(0); - world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100)); + world.updatePosition({ userId: 2 }, new Point(100+160+160+1, 100)); expect(disconnectCallNumber).toBe(2); - world.updatePosition(user2, new Point(262, 100)); + world.updatePosition({ userId: 2 }, new Point(262, 100)); expect(disconnectCallNumber).toBe(2); }); -}); + +}) diff --git a/back/tests/MapFetcherTest.ts b/back/tests/MapFetcherTest.ts deleted file mode 100644 index 1e7ca447..00000000 --- a/back/tests/MapFetcherTest.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { arrayIntersect } from "../src/Services/ArrayHelper"; -import { mapFetcher } from "../src/Services/MapFetcher"; - -describe("MapFetcher", () => { - it("should return true on localhost ending URLs", async () => { - expect(await mapFetcher.isLocalUrl("https://localhost")).toBeTrue(); - expect(await mapFetcher.isLocalUrl("https://foo.localhost")).toBeTrue(); - }); - - it("should return true on DNS resolving to a local domain", async () => { - expect(await mapFetcher.isLocalUrl("https://127.0.0.1.nip.io")).toBeTrue(); - }); - - it("should return true on an IP resolving to a local domain", async () => { - expect(await mapFetcher.isLocalUrl("https://127.0.0.1")).toBeTrue(); - expect(await mapFetcher.isLocalUrl("https://192.168.0.1")).toBeTrue(); - }); - - it("should return false on an IP resolving to a global domain", async () => { - expect(await mapFetcher.isLocalUrl("https://51.12.42.42")).toBeFalse(); - }); - - it("should return false on an DNS resolving to a global domain", async () => { - expect(await mapFetcher.isLocalUrl("https://maps.workadventu.re")).toBeFalse(); - }); - - it("should throw error on invalid domain", async () => { - await expectAsync( - mapFetcher.isLocalUrl("https://this.domain.name.doesnotexistfoobgjkgfdjkgldf.com") - ).toBeRejected(); - }); -}); diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index bf7ddd6e..573a3233 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -1,10 +1,15 @@ import "jasmine"; -import { PositionNotifier } from "../src/Model/PositionNotifier"; -import { User, UserSocket } from "../src/Model/User"; -import { Zone } from "_Model/Zone"; -import { Movable } from "_Model/Movable"; -import { PositionInterface } from "_Model/PositionInterface"; -import { ZoneSocket } from "../src/RoomManager"; +import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom"; +import {Point} from "../src/Model/Websocket/MessageUserPosition"; +import { Group } from "../src/Model/Group"; +import {PositionNotifier} from "../src/Model/PositionNotifier"; +import {User} from "../src/Model/User"; +import {PointInterface} from "../src/Model/Websocket/PointInterface"; +import {Zone} from "_Model/Zone"; +import {Movable} from "_Model/Movable"; +import {PositionInterface} from "_Model/PositionInterface"; +import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; + describe("PositionNotifier", () => { it("should receive notifications when player moves", () => { @@ -12,86 +17,50 @@ describe("PositionNotifier", () => { let moveTriggered = false; let leaveTriggered = false; - const positionNotifier = new PositionNotifier( - 300, - 300, - (thing: Movable) => { - enterTriggered = true; - }, - (thing: Movable, position: PositionInterface) => { - moveTriggered = true; - }, - (thing: Movable) => { - leaveTriggered = true; - }, - () => {}, - () => {} - ); + const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => { + enterTriggered = true; + }, (thing: Movable, position: PositionInterface) => { + moveTriggered = true; + }, (thing: Movable) => { + leaveTriggered = true; + }); - const user1 = new User( - 1, - "test", - "10.0.0.2", - { - x: 500, - y: 500, - moving: false, - direction: "down", - }, - false, - positionNotifier, - {} as UserSocket, - [], - null, - "foo", - [] - ); + const user1 = new User(1, 'test', { + x: 500, + y: 500, + moving: false, + direction: 'down' + }, false, positionNotifier, {} as ExSocketInterface); - const user2 = new User( - 2, - "test", - "10.0.0.2", - { - x: -9999, - y: -9999, - moving: false, - direction: "down", - }, - false, - positionNotifier, - {} as UserSocket, - [], - null, - "foo", - [] - ); + const user2 = new User(2, 'test', { + x: -9999, + y: -9999, + moving: false, + direction: 'down' + }, false, positionNotifier, {} as ExSocketInterface); - positionNotifier.addZoneListener({} as ZoneSocket, 0, 0); - positionNotifier.addZoneListener({} as ZoneSocket, 0, 1); - positionNotifier.addZoneListener({} as ZoneSocket, 1, 1); - positionNotifier.addZoneListener({} as ZoneSocket, 1, 0); - /*positionNotifier.setViewport(user1, { + positionNotifier.setViewport(user1, { left: 200, right: 600, top: 100, bottom: 500 - });*/ + }); - user2.setPosition({ x: 500, y: 500, direction: "down", moving: false }); + user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); expect(enterTriggered).toBe(true); expect(moveTriggered).toBe(false); enterTriggered = false; // Move inside the zone - user2.setPosition({ x: 501, y: 500, direction: "down", moving: false }); + user2.setPosition({x:501, y:500, direction: 'down', moving: false}); expect(enterTriggered).toBe(false); expect(moveTriggered).toBe(true); moveTriggered = false; // Move out of the zone in a zone that we don't track - user2.setPosition({ x: 901, y: 500, direction: "down", moving: false }); + user2.setPosition({x: 901, y: 500, direction: 'down', moving: false}); expect(enterTriggered).toBe(false); expect(moveTriggered).toBe(false); @@ -99,15 +68,22 @@ describe("PositionNotifier", () => { leaveTriggered = false; // Move back in - user2.setPosition({ x: 500, y: 500, direction: "down", moving: false }); + user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); expect(enterTriggered).toBe(true); expect(moveTriggered).toBe(false); expect(leaveTriggered).toBe(false); enterTriggered = false; + // Move out of the zone in a zone that we do track + user2.setPosition({x: 200, y: 500, direction: 'down', moving: false}); + expect(enterTriggered).toBe(false); + expect(moveTriggered).toBe(true); + expect(leaveTriggered).toBe(false); + moveTriggered = false; + // Leave the room positionNotifier.leave(user2); - //positionNotifier.removeViewport(user2); + positionNotifier.removeViewport(user2); expect(enterTriggered).toBe(false); expect(moveTriggered).toBe(false); expect(leaveTriggered).toBe(true); @@ -119,128 +95,82 @@ describe("PositionNotifier", () => { let moveTriggered = false; let leaveTriggered = false; - const positionNotifier = new PositionNotifier( - 300, - 300, - (thing: Movable, fromZone: Zone | null) => { - enterTriggered = true; - }, - (thing: Movable, position: PositionInterface) => { - moveTriggered = true; - }, - (thing: Movable) => { - leaveTriggered = true; - }, - () => {}, - () => {} - ); + const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => { + enterTriggered = true; + }, (thing: Movable, position: PositionInterface) => { + moveTriggered = true; + }, (thing: Movable) => { + leaveTriggered = true; + }); - const user1 = new User( - 1, - "test", - "10.0.0.2", - { - x: 500, - y: 500, - moving: false, - direction: "down", - }, - false, - positionNotifier, - {} as UserSocket, - [], - null, - "foo", - [] - ); + const user1 = new User(1, 'test', { + x: 500, + y: 500, + moving: false, + direction: 'down' + }, false, positionNotifier, {} as ExSocketInterface); - const user2 = new User( - 2, - "test", - "10.0.0.2", - { - x: 0, - y: 0, - moving: false, - direction: "down", - }, - false, - positionNotifier, - {} as UserSocket, - [], - null, - "foo", - [] - ); + const user2 = new User(2, 'test', { + x: 0, + y: 0, + moving: false, + direction: 'down' + }, false, positionNotifier, {} as ExSocketInterface); - const listener = {} as ZoneSocket; - positionNotifier.addZoneListener(listener, 0, 0); - positionNotifier.addZoneListener(listener, 0, 1); - positionNotifier.addZoneListener(listener, 1, 1); - positionNotifier.addZoneListener(listener, 1, 0); - /*let newUsers = positionNotifier.setViewport(user1, { + let newUsers = positionNotifier.setViewport(user1, { left: 200, right: 600, top: 100, bottom: 500 - });*/ - positionNotifier.enter(user1); - positionNotifier.enter(user2); + }); - //expect(newUsers.length).toBe(2); + expect(newUsers.length).toBe(2); expect(enterTriggered).toBe(true); enterTriggered = false; - //positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0}) - user2.setPosition({ x: 500, y: 500, direction: "down", moving: false }); + user2.setPosition({x: 500, y: 500, direction: 'down', moving: false}); - expect(enterTriggered).toBe(true); - expect(moveTriggered).toBe(false); - expect(leaveTriggered).toBe(true); - enterTriggered = false; - leaveTriggered = false; + expect(enterTriggered).toBe(false); + expect(moveTriggered).toBe(true); + moveTriggered = false; - // Add a listener, but the user in not in this zone. - positionNotifier.addZoneListener(listener, 10, 10); - - /*positionNotifier.setViewport(user1, { + // Move the viewport but the user stays inside. + positionNotifier.setViewport(user1, { left: 201, right: 601, top: 100, bottom: 500 - });*/ + }); expect(enterTriggered).toBe(false); expect(moveTriggered).toBe(false); expect(leaveTriggered).toBe(false); - // Stop listening to zone - positionNotifier.removeZoneListener(listener, 1, 1); // Move the viewport out of the user. - /*positionNotifier.setViewport(user1, { + positionNotifier.setViewport(user1, { left: 901, right: 1001, top: 100, bottom: 500 - });*/ + }); expect(enterTriggered).toBe(false); expect(moveTriggered).toBe(false); - expect(leaveTriggered).toBe(false); + expect(leaveTriggered).toBe(true); + leaveTriggered = false; // Move the viewport back on the user. - positionNotifier.addZoneListener(listener, 1, 1); - /*newUsers = positionNotifier.setViewport(user1, { + newUsers = positionNotifier.setViewport(user1, { left: 200, right: 600, top: 100, bottom: 500 - });*/ + }); - expect(enterTriggered).toBe(false); + expect(enterTriggered).toBe(true); expect(moveTriggered).toBe(false); expect(leaveTriggered).toBe(false); enterTriggered = false; - //expect(newUsers.length).toBe(2); + expect(newUsers.length).toBe(2); }); -}); +}) diff --git a/back/tests/RoomIdentifierTest.ts b/back/tests/RoomIdentifierTest.ts new file mode 100644 index 00000000..c3817ff7 --- /dev/null +++ b/back/tests/RoomIdentifierTest.ts @@ -0,0 +1,19 @@ +import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier"; + +describe("RoomIdentifier", () => { + it("should flag public id as anonymous", () => { + expect(isRoomAnonymous('_/global/test')).toBe(true); + }); + it("should flag public id as not anonymous", () => { + expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false); + }); + it("should extract roomSlug from public ID", () => { + expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json'); + }); + it("should extract correct from private ID", () => { + const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor'); + expect(organizationSlug).toBe('afup'); + expect(worldSlug).toBe('afup2020'); + expect(roomSlug).toBe('1floor'); + }); +}) \ No newline at end of file diff --git a/back/tests/getNearbyDescriptorsMatrixTest.ts b/back/tests/getNearbyDescriptorsMatrixTest.ts deleted file mode 100644 index 6c81db76..00000000 --- a/back/tests/getNearbyDescriptorsMatrixTest.ts +++ /dev/null @@ -1,67 +0,0 @@ -import "jasmine"; -import { getNearbyDescriptorsMatrix } from "../src/Model/PositionNotifier"; - -describe("getNearbyDescriptorsMatrix", () => { - it("should create a matrix of coordinates in a square around the parameter", () => { - const matrix = []; - for (const d of getNearbyDescriptorsMatrix({ i: 1, j: 1 })) { - matrix.push(d); - } - - expect(matrix).toEqual([ - { i: 0, j: 0 }, - { i: 1, j: 0 }, - { i: 2, j: 0 }, - { i: 0, j: 1 }, - { i: 1, j: 1 }, - { i: 2, j: 1 }, - { i: 0, j: 2 }, - { i: 1, j: 2 }, - { i: 2, j: 2 }, - ]); - }); - - it("should create a matrix of coordinates in a square around the parameter bis", () => { - const matrix = []; - for (const d of getNearbyDescriptorsMatrix({ i: 8, j: 3 })) { - matrix.push(d); - } - - expect(matrix).toEqual([ - { i: 7, j: 2 }, - { i: 8, j: 2 }, - { i: 9, j: 2 }, - { i: 7, j: 3 }, - { i: 8, j: 3 }, - { i: 9, j: 3 }, - { i: 7, j: 4 }, - { i: 8, j: 4 }, - { i: 9, j: 4 }, - ]); - }); - - it("should not create a matrix with negative coordinates", () => { - const matrix = []; - for (const d of getNearbyDescriptorsMatrix({ i: 0, j: 0 })) { - matrix.push(d); - } - - expect(matrix).toEqual([ - { i: 0, j: 0 }, - { i: 1, j: 0 }, - { i: 0, j: 1 }, - { i: 1, j: 1 }, - ]); - }); - - /*it("should not create a matrix with coordinates bigger than its dimmensions", () => { - const matrix = getNearbyDescriptorsMatrix({i: 4, j: 4}, 5, 5); - - expect(matrix).toEqual([ - {i: 3,j: 3}, - {i: 4,j: 3}, - {i: 3,j: 4}, - {i: 4,j: 4}, - ]) - });*/ -}); diff --git a/back/tsconfig.json b/back/tsconfig.json index e149d304..6972715f 100644 --- a/back/tsconfig.json +++ b/back/tsconfig.json @@ -3,7 +3,7 @@ "experimentalDecorators": true, /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "downlevelIteration": true, "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ diff --git a/back/yarn.lock b/back/yarn.lock index 31bd1a09..1bd7c802 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -3,122 +3,65 @@ "@babel/code-frame@^7.0.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" - integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: - "@babel/highlight" "^7.16.0" + "@babel/highlight" "^7.10.4" -"@babel/helper-validator-identifier@^7.15.7": - version "7.15.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== -"@babel/highlight@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" - integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== dependencies: - "@babel/helper-validator-identifier" "^7.15.7" + "@babel/helper-validator-identifier" "^7.10.4" chalk "^2.0.0" js-tokens "^4.0.0" -"@eslint/eslintrc@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" - integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.2.0" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" -"@humanwhocodes/config-array@^0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914" - integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA== - 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== - -"@mapbox/node-pre-gyp@^1.0.4": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz#32abc8a5c624bc4e46c43d84dfb8b26d33a96f58" - integrity sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg== - dependencies: - detect-libc "^1.0.3" - https-proxy-agent "^5.0.0" - make-dir "^3.1.0" - node-fetch "^2.6.5" - nopt "^5.0.0" - npmlog "^5.0.1" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.11" - -"@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" +"@nodelib/fs.stat@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" + integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== "@types/busboy@^0.2.3": - version "0.2.4" - resolved "https://registry.yarnpkg.com/@types/busboy/-/busboy-0.2.4.tgz#19922f8c7076ad6d47b2565da8c0a94c88776315" - integrity sha512-f+ZCVjlcN8JW/zf3iR0GqO4gjOUlltMTtZjn+YR1mlK+MVu6esTiIecO0/GQlmYQPQLdBnc7+5vG3Xb+SkvFLw== + version "0.2.3" + resolved "https://registry.yarnpkg.com/@types/busboy/-/busboy-0.2.3.tgz#6697ad29873246c530f09a3ff5a40861824230d5" + integrity sha1-ZpetKYcyRsUw8Jo/9aQIYYJCMNU= dependencies: "@types/node" "*" -"@types/bytebuffer@^5.0.40": - version "5.0.42" - resolved "https://registry.yarnpkg.com/@types/bytebuffer/-/bytebuffer-5.0.42.tgz#1c602a77942d34c5c0879ad75c58d5d8c07dfb3b" - integrity sha512-lEgKojWUAc/MG2t649oZS5AfYFP2xRNPoDuwDBlBMjHXd8MaGPgFgtCXUK7inZdBOygmVf10qxc1Us8GXC96aw== - dependencies: - "@types/long" "*" - "@types/node" "*" - "@types/circular-json@^0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@types/circular-json/-/circular-json-0.4.0.tgz#7401f7e218cfe87ad4c43690da5658b9acaf51be" integrity sha512-7+kYB7x5a7nFWW1YPBh3KxhwKfiaI4PbZ1RvzBU91LZy7lWJO822CI+pqzSre/DZ7KsCuMKdHnLHHFu8AyXbQg== -"@types/debug@^4.1.5": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" - integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== - dependencies: - "@types/ms" "*" +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== "@types/google-protobuf@^3.7.3": - version "3.15.5" - resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.5.tgz#644b2be0f5613b1f822c70c73c6b0e0b5b5fa2ad" - integrity sha512-6bgv24B+A2bo9AfzReeg5StdiijKzwwnRflA8RLd1V4Yv995LeTmo0z69/MPbBDFSiZWdZHQygLo/ccXhMEDgw== + version "3.7.3" + resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.7.3.tgz#429512e541bbd777f2c867692e6335ee08d1f6d4" + integrity sha512-FRwj40euE2bYkG+0X5w2nEA8yAzgJRcEa7RBd0Gsdkb9/tPM2pctVVAvnOUTbcXo2VmIHPo0Ae94Gl9vRHfKzg== "@types/http-status-codes@^1.2.0": version "1.2.0" @@ -128,55 +71,33 @@ http-status-codes "*" "@types/jasmine@^3.5.10": - version "3.10.2" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.10.2.tgz#1b9f9ba9ad7bfd7d322f7ed9d8753220b1c84b52" - integrity sha512-qs4xjVm4V/XjM6owGm/x6TNmhGl5iKX8dkTdsgdgl9oFnqgzxLepnS7rN9Tdo7kDmnFD/VEqKrW57cGD2odbEg== + version "3.5.14" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.14.tgz#f41a14e8ffa939062a71cf9722e5ee7d4e1f94af" + integrity sha512-Fkgk536sHPqcOtd+Ow+WiUNuk0TSo/BntKkF8wSvcd6M2FvPjeXcUE6Oz/bwDZiUZEaXLslAgw00Q94Pnx6T4w== -"@types/json-schema@^7.0.9": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/json-schema@^7.0.3": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== "@types/jsonwebtoken@^8.3.8": - version "8.5.6" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.6.tgz#1913e5a61e70a192c5a444623da4901a7b1a9d42" - integrity sha512-+P3O/xC7nzVizIi5VbF34YtqSonFsdnbXBnWUCYRiKOi1f9gA4sEFvXkrGr/QVV23IbMYvcoerI7nnhDUiWXRQ== + version "8.5.0" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5" + integrity sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg== dependencies: "@types/node" "*" -"@types/long@*": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" - integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== - "@types/mkdirp@^1.0.1": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.2.tgz#8d0bad7aa793abe551860be1f7ae7f3198c16666" - integrity sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ== + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.1.tgz#0930b948914a78587de35458b86c907b6e98bbf6" + integrity sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q== dependencies: "@types/node" "*" -"@types/ms@*": - version "0.7.31" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== - "@types/node@*": - version "17.0.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.4.tgz#fec0ce0526abb6062fd206d72a642811b887a111" - integrity sha512-6xwbrW4JJiJLgF+zNypN5wr2ykM9/jHcL7rQ8fZe2vuftggjzZeRSM4OwRc6Xk8qWjwJ99qVHo/JgOGmomWRog== - -"@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/redis@^2.8.31": - version "2.8.32" - resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11" - integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w== - dependencies: - "@types/node" "*" + version "14.11.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" + integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== "@types/strip-bom@^3.0.0": version "3.0.0" @@ -188,10 +109,10 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== -"@types/uuid@8.3.1": - version "8.3.1" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" - integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== +"@types/uuid@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" + integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== "@types/uuidv4@^5.0.0": version "5.0.0" @@ -200,230 +121,217 @@ dependencies: uuidv4 "*" -"@typescript-eslint/eslint-plugin@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.0.tgz#52cd9305ceef98a5333f9492d519e6c6c7fe7d43" - integrity sha512-spu1UW7QuBn0nJ6+psnfCc3iVoQAifjKORgBngKOmC8U/1tbe2YJMzYQqDGYB4JCss7L8+RM2kKLb1B1Aw9BNA== +"@typescript-eslint/eslint-plugin@^2.26.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" + integrity sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ== dependencies: - "@typescript-eslint/experimental-utils" "5.8.0" - "@typescript-eslint/scope-manager" "5.8.0" - debug "^4.3.2" + "@typescript-eslint/experimental-utils" "2.34.0" functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.2.0" - semver "^7.3.5" - tsutils "^3.21.0" + regexpp "^3.0.0" + tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.0.tgz#0916ffe98d34b3c95e3652efa0cace61a7b25728" - integrity sha512-KN5FvNH71bhZ8fKtL+lhW7bjm7cxs1nt+hrDZWIqb6ViCffQcWyLunGrgvISgkRojIDcXIsH+xlFfI4RCDA0xA== +"@typescript-eslint/experimental-utils@2.34.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" + integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.8.0" - "@typescript-eslint/types" "5.8.0" - "@typescript-eslint/typescript-estree" "5.8.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.34.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" -"@typescript-eslint/parser@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.8.0.tgz#b39970b21c1d7bc4a6018507fb29b380328d2587" - integrity sha512-Gleacp/ZhRtJRYs5/T8KQR3pAQjQI89Dn/k+OzyCKOsLiZH2/Vh60cFBTnFsHNI6WAD+lNUo/xGZ4NeA5u0Ipw== +"@typescript-eslint/parser@^2.26.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8" + integrity sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA== dependencies: - "@typescript-eslint/scope-manager" "5.8.0" - "@typescript-eslint/types" "5.8.0" - "@typescript-eslint/typescript-estree" "5.8.0" - debug "^4.3.2" + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.34.0" + "@typescript-eslint/typescript-estree" "2.34.0" + eslint-visitor-keys "^1.1.0" -"@typescript-eslint/scope-manager@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.8.0.tgz#2371095b4fa4c7be6a80b380f4e1b49c715e16f4" - integrity sha512-x82CYJsLOjPCDuFFEbS6e7K1QEWj7u5Wk1alw8A+gnJiYwNnDJk0ib6PCegbaPMjrfBvFKa7SxE3EOnnIQz2Gg== +"@typescript-eslint/typescript-estree@2.34.0": + version "2.34.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" + integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== dependencies: - "@typescript-eslint/types" "5.8.0" - "@typescript-eslint/visitor-keys" "5.8.0" + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" -"@typescript-eslint/types@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.8.0.tgz#e7fa74ec35d9dbe3560d039d3d8734986c3971e0" - integrity sha512-LdCYOqeqZWqCMOmwFnum6YfW9F3nKuxJiR84CdIRN5nfHJ7gyvGpXWqL/AaW0k3Po0+wm93ARAsOdzlZDPCcXg== +acorn-jsx@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== -"@typescript-eslint/typescript-estree@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.0.tgz#900469ba9d5a37f4482b014ecce4a5dbb86cb4dd" - integrity sha512-srfeZ3URdEcUsSLbkOFqS7WoxOqn8JNil2NSLO9O+I2/Uyc85+UlfpEvQHIpj5dVts7KKOZnftoJD/Fdv0L7nQ== - dependencies: - "@typescript-eslint/types" "5.8.0" - "@typescript-eslint/visitor-keys" "5.8.0" - debug "^4.3.2" - globby "^11.0.4" - is-glob "^4.0.3" - semver "^7.3.5" - tsutils "^3.21.0" +acorn@^7.1.1: + version "7.4.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" + integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== -"@typescript-eslint/visitor-keys@5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.0.tgz#22d4ed96fe2451135299239feedb9fe1dcec780c" - integrity sha512-+HDIGOEMnqbxdAHegxvnOqESUH6RWFRR2b8qxP1W9CZnnYh4Usz6MBL+2KMAgPk/P0o9c1HqnYtwzVH6GTIqug== - dependencies: - "@typescript-eslint/types" "5.8.0" - eslint-visitor-keys "^3.0.0" - -"@workadventure/tiled-map-type-guard@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.3.tgz#62c2061cacbe1360b84162af0b7e4639ed8bfa7e" - integrity sha512-pUMxBBZHYAFkpnGWZAVAE8+M+Wn9UtzqZhXvBBBbB1gEakHIka7ahdTGfh0DgRaWrVszVXOP3tf49Dhdmn9pDg== - dependencies: - generic-type-guard "^3.4.1" - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -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@^8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" - integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== - -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@^6.10.0, ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== +ajv@^6.10.0, ajv@^6.10.2: + version "6.12.5" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== 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" -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-escapes@^4.3.0: - 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== +ansi-escapes@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== dependencies: - type-fest "^0.21.3" + type-fest "^0.11.0" -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -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@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-styles@^3.2.1: +ansi-styles@^3.2.0, 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== +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== dependencies: + "@types/color-name" "^1.1.1" color-convert "^2.0.1" -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== +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" - integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -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== - -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== - -ascli@~1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ascli/-/ascli-1.0.1.tgz#bcfa5974a62f18e81cabaeb49732ab4a88f906bc" - integrity sha1-vPpZdKYvGOgcq660lzKrSoj5Brw= +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: - colour "~0.7.1" - optjs "~3.2.2" + sprintf-js "~1.0.2" -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= -axios@^0.21.2: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +axios@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd" + integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA== dependencies: - follow-redirects "^1.14.0" + follow-redirects "^1.10.0" 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== + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" 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== + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== bintrees@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= +body-parser@^1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -432,7 +340,23 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1, braces@~3.0.2: +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +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== @@ -445,9 +369,17 @@ buffer-equal-constant-time@1.0.1: integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= 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== + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" busboy@^0.3.1: version "0.3.1" @@ -456,24 +388,50 @@ busboy@^0.3.1: dependencies: dicer "0.3.0" -bytebuffer@~5: - version "5.0.1" - resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" - integrity sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== dependencies: - long "~3" + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= 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== -camelcase@^2.0.1: +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.1.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -482,43 +440,48 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== +chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +chokidar@^3.4.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== dependencies: - anymatch "~3.1.2" + anymatch "~3.1.1" braces "~3.0.2" - glob-parent "~5.1.2" + glob-parent "~5.1.0" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.6.0" + readdirp "~3.4.0" optionalDependencies: - fsevents "~2.3.2" - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + fsevents "~2.1.2" circular-json@^0.5.9: version "0.5.9" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ== -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== +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" cli-cursor@^3.1.0: version "3.1.0" @@ -527,27 +490,18 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-truncate@2.1.0, cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -cliui@^3.0.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + map-visit "^1.0.0" + object-visit "^1.0.0" color-convert@^1.9.0: version "1.9.3" @@ -573,74 +527,82 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -colorette@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" - integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== - -colorette@^2.0.16: - version "2.0.16" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" - integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== - -colour@~0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" - integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= - -commander@^8.2.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== 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= -console-control-strings@^1.0.0, console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -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== +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== 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" + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +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== -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== +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" -debug@4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +dateformat@~1.0.4-1.2.3: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: + 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.0.1, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== dependencies: ms "2.1.2" -decamelize@^1.1.1: +decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -650,25 +612,45 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -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== +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -delegates@^1.0.0: +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" -denque@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" dicer@0.3.0: version "0.3.0" @@ -682,13 +664,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -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" - doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -710,19 +685,22 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" +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= + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + 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== -enquirer@^2.3.5, enquirer@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -error-ex@^1.3.1: +error-ex@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -734,12 +712,7 @@ escape-string-regexp@^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@^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== - -eslint-scope@^5.1.1: +eslint-scope@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -747,88 +720,86 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" - integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" + eslint-visitor-keys "^1.1.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-utils@^2.0.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.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2" - integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA== - -eslint@^8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.5.0.tgz#ddd2c1afd8f412036f87ae2a063d2aa296d3175f" - integrity sha512-tVGSkgNbOfiHyVte8bCM8OmX+xG9PzVG/B4UCF60zx7j61WIVY/AqJECDgpLD4DbbESD0e174gOg3ZlrX15GDg== + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== dependencies: - "@eslint/eslintrc" "^1.0.5" - "@humanwhocodes/config-array" "^0.9.2" + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== + dependencies: + "@babel/code-frame" "^7.0.0" ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" doctrine "^3.0.0" - enquirer "^2.3.5" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.0" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.1.0" - espree "^9.2.0" - esquery "^1.4.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" + file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" + glob-parent "^5.0.0" + globals "^12.1.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" + inquirer "^7.0.0" is-glob "^4.0.0" - js-yaml "^4.1.0" + js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" + levn "^0.3.0" + lodash "^4.17.14" minimatch "^3.0.4" + mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.9.1" + optionator "^0.8.3" progress "^2.0.0" - regexpp "^3.2.0" - semver "^7.2.1" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.2.0.tgz#c50814e01611c2d0f8bd4daa83c369eabba80dbc" - integrity sha512-oP3utRkynpZWF/F2x/HZJ+AGtnIclaR7z1pYPxy7NYM2fSO6LgK/Rkny8anRSPK/VwEA1eqm2squui0T7ZMOBg== +espree@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== dependencies: - acorn "^8.6.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.1.0" + acorn "^7.1.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.1.0" -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esprima@^4.0.0: + 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.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== dependencies: estraverse "^5.1.0" @@ -845,69 +816,116 @@ estraverse@^4.1.1: integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== 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== -execa@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= 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" + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +fast-deep-equal@^3.1.1: 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.1.1: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== +fast-glob@^2.2.6: + version "2.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" + integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== 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" + "@mrmlnc/readdir-enhanced" "^2.2.1" + "@nodelib/fs.stat" "^1.1.2" + glob-parent "^3.1.0" + is-glob "^4.0.0" + merge2 "^1.2.3" + micromatch "^3.1.10" fast-json-stable-stringify@^2.0.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== +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: - reusify "^1.0.4" + escape-string-regexp "^1.0.5" -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== +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== dependencies: - flat-cache "^3.0.4" + flat-cache "^2.0.1" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" fill-range@^7.0.1: version "7.0.1" @@ -916,104 +934,99 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -filter-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" - integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= - -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== +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" + path-exists "^2.0.0" + pinkie-promise "^2.0.0" -flatted@^3.1.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.4.tgz#28d9969ea90661b5134259f312ab6aa7929ac5e2" - integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== - -follow-redirects@^1.14.0: - version "1.14.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" - integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== dependencies: - minipass "^3.0.0" + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + +follow-redirects@^1.10.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" 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: - 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== +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== 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= -gauge@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" - integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== +generic-type-guard@^3.2.0: + version "3.3.3" + resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1" + integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ== + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.2" - console-control-strings "^1.0.0" - has-unicode "^2.0.1" - object-assign "^4.1.1" - signal-exit "^3.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.2" + is-glob "^3.1.0" + path-dirname "^1.0.0" -generic-type-guard@^3.2.0, generic-type-guard@^3.4.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.5.0.tgz#39de9f8fceee65d79e7540959f0e7b23210c07b6" - integrity sha512-OpgXv/sbRobhFboaSyN/Tsh97Sxt5pcfLLxCiYZgYIIWFFp+kn2EzAXiaQZKEVRlq1rOE/zh8cYhJXEwplbJiQ== - -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-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== - -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== +glob-parent@^5.0.0, glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1: - 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.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= -glob@^7.0.5, glob@^7.1.3, glob@^7.1.6: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== +glob@^7.1.3, glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -1022,41 +1035,22 @@ glob@^7.0.5, glob@^7.1.3, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^13.6.0, globals@^13.9.0: - version "13.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.0.tgz#4d733760304230a0082ed96e21e5c565f898089e" - integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== dependencies: - type-fest "^0.20.2" - -globby@^11.0.4: - version "11.0.4" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" - integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" + type-fest "^0.8.1" google-protobuf@^3.13.0: - version "3.19.1" - resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.1.tgz#5af5390e8206c446d8f49febaffd4b7f4ac28f41" - integrity sha512-Isv1RlNC+IzZzilcxnlVSf+JvuhxmY7DaxYCBy+zPS9XVuJRtlTTIXR9hnZ1YL1MMusJn/7eSy2swCzZIomQSg== + version "3.13.0" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.13.0.tgz#909c5983d75dd6101ed57c79e0528d000cdc3251" + integrity sha512-ZIf3qfLFayVrPvAjeKKxO5FRF1/NwRxt6Dko+fWEMuHwHbZx8/fcaAao9b0wCM6kr8qeg2te8XTpyuvKuD9aKw== -grpc@^1.24.4: - version "1.24.11" - resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.24.11.tgz#7039da9f6f22ce35168535a6d5dda618398a5966" - integrity sha512-8/AQdFCzCeCDWW3SoaMNp6ccbRvTQEH1O1u1uFtt29eWsg5gSZCJ3m6fbkduEIh3smY7WAPP+LgVJ5n3nZRxcA== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.4" - "@types/bytebuffer" "^5.0.40" - lodash.camelcase "^4.3.0" - lodash.clone "^4.5.0" - nan "^2.13.2" - protobufjs "^5.0.3" +graceful-fs@^4.1.2: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== has-flag@^3.0.0: version "3.0.0" @@ -1068,50 +1062,79 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -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== +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= dependencies: - function-bind "^1.1.1" + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" http-status-codes@*: version "2.1.4" resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== -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" +http-status-codes@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-1.4.0.tgz#6e4c15d16ff3a9e2df03b89f3a55e1aae05fb477" + integrity sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ== -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, 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" ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.4, ignore@^5.1.8: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -import-fresh@^3.0.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== +import-fresh@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -1121,10 +1144,12 @@ imurmurhash@^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== +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" inflight@^1.0.4: version "1.0.6" @@ -1134,20 +1159,48 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@2, inherits@^2.0.3, inherits@~2.0.1, 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== -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -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== +inquirer@^7.0.0: + version "7.3.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" is-arrayish@^0.2.1: version "0.2.1" @@ -1161,91 +1214,175 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-core-module@^2.2.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" - integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== - dependencies: - has "^1.0.3" +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-extglob@^2.1.1: +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, 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@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" +is-finite@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 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-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== +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== dependencies: is-extglob "^2.1.1" +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + 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-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" -is-regexp@^1.0.0: +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +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, isarray@~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-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== + 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= -jasmine-core@~3.10.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.10.1.tgz#7aa6fa2b834a522315c651a128d940eca553989a" - integrity sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA== +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +iterall@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" + integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== + +jasmine-core@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20" + integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw== jasmine@^3.5.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.10.0.tgz#acd3cd560a9d20d8fdad6bd2dd05867d188503f3" - integrity sha512-2Y42VsC+3CQCTzTwJezOvji4qLORmKIE0kwowWC+934Krn6ZXNQYljiwK5st9V3PVx96BSiDYXSB60VVah3IlQ== + version "3.6.1" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.6.1.tgz#a20456b309a669b547a3c24bb2120f16f70cfc65" + integrity sha512-Jqp8P6ZWkTVFGmJwBK46p+kJNrZCdqkQ4GL+PGuBXZwK1fM4ST9BizkYgIwCFqYYqnTizAy6+XG2Ej5dFrej9Q== dependencies: - glob "^7.1.6" - jasmine-core "~3.10.0" + fast-glob "^2.2.6" + jasmine-core "~3.6.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@^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== +js-yaml@^3.13.1: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: - argparse "^2.0.1" - -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== + argparse "^1.0.7" + esprima "^4.0.0" json-schema-traverse@^0.4.1: version "0.4.1" @@ -1290,69 +1427,48 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= dependencies: - invert-kv "^1.0.0" + is-buffer "^1.1.5" -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== +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" + is-buffer "^1.1.5" -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== +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -lint-staged@^11.0.0: - version "11.2.6" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.2.6.tgz#f477b1af0294db054e5937f171679df63baa4c43" - integrity sha512-Vti55pUnpvPE0J9936lKl0ngVeTdSZpEdTNhASbkaWX7J5R9OEifo1INBGQuGW4zmy6OG+TcWPJ3m5yuy5Q8Tg== +kind-of@^6.0.0, 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== + +levn@^0.3.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: - cli-truncate "2.1.0" - colorette "^1.4.0" - commander "^8.2.0" - cosmiconfig "^7.0.1" - debug "^4.3.2" - enquirer "^2.3.6" - execa "^5.1.1" - listr2 "^3.12.2" - micromatch "^4.0.4" - normalize-path "^3.0.0" - please-upgrade-node "^3.2.0" - string-argv "0.3.1" - stringify-object "3.3.0" - supports-color "8.1.1" + prelude-ls "~1.1.2" + type-check "~0.3.2" -listr2@^3.12.2: - version "3.13.5" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.13.5.tgz#105a813f2eb2329c4aae27373a281d610ee4985f" - integrity sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA== +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= dependencies: - cli-truncate "^2.1.0" - colorette "^2.0.16" - log-update "^4.0.0" - p-map "^4.0.0" - rfdc "^1.3.0" - rxjs "^7.4.0" - through "^2.3.8" - wrap-ansi "^7.0.0" - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - -lodash.clone@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" - integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" lodash.includes@^4.3.0: version "4.3.0" @@ -1384,67 +1500,102 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= -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.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -log-update@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" - integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== - dependencies: - ansi-escapes "^4.3.0" - cli-cursor "^3.1.0" - slice-ansi "^4.0.0" - wrap-ansi "^6.2.0" +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -long@~3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" - integrity sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s= - -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== +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= dependencies: - yallist "^4.0.0" - -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" + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -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== +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -merge2@^1.3.0: +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +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= + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge2@^1.2.3: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== +micromatch@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" mimic-fn@^2.1.0: version "2.1.0" @@ -1458,97 +1609,137 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.5: +minimist@^1.1.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minipass@^3.0.0: - version "3.1.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" - integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== dependencies: - yallist "^4.0.0" + for-in "^1.0.2" + is-extendable "^1.0.1" -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: - minipass "^3.0.0" - yallist "^4.0.0" + minimist "^1.2.5" -mkdirp@^1.0.3, mkdirp@^1.0.4: +mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -ms@2.1.2: +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, ms@^2.1.1: 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.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multer@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.1" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" -nan@^2.13.2: - version "2.15.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" - integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" 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= -node-fetch@^2.6.5: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== dependencies: - abbrev "1" + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" 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== -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" - -npmlog@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" - integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== - dependencies: - are-we-there-yet "^2.0.0" - console-control-strings "^1.1.0" - gauge "^3.0.0" - set-blocking "^2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -object-assign@^4.1.1: +object-assign@^4.0.1, 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-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +on-finished@^2.3.0, 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" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1556,43 +1747,29 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^5.1.0, onetime@^5.1.2: +onetime@^5.1.0: 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" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.8.3: + 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.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" + 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" -optjs@~3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee" - integrity sha1-aabOicRCpEQDFBrS+bNwvVu29O4= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -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" +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= parent-module@^1.0.0: version "1.0.1" @@ -1601,57 +1778,90 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^5.0.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== +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= 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" + error-ex "^1.2.0" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" 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-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== -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== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -please-upgrade-node@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" - integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= dependencies: - semver-compare "^1.0.0" + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" -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== +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== -prettier@^2.3.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" - integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +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= + +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== progress@^2.0.0: version "2.0.3" @@ -1665,95 +1875,140 @@ prom-client@^12.0.0: dependencies: tdigest "^0.1.1" -protobufjs@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-5.0.3.tgz#e4dfe9fb67c90b2630d15868249bcc4961467a17" - integrity sha512-55Kcx1MhPZX0zTbVosMQEO5R6/rikNXd9b6RQK4KSPcrSIIwoXTtebIczUrXlwaSrbz4x8XUVThGPob1n8I4QA== - dependencies: - ascli "~1" - bytebuffer "~5" - glob "^7.0.5" - yargs "^3.10.0" - punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + query-string@^6.13.3: - version "6.14.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" - integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== + version "6.13.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.4.tgz#b35a9a3bd4955bce55f94feb0e819b3d0be6f66f" + integrity sha512-E2NPIeJoBEJGQNy3ib1k/Z/OkDBUKIo8IV2ZVwbKfoa65IS9unqWWUlLcbfU70Da0qNoxUZZA8CfKUjKLE641Q== dependencies: decode-uri-component "^0.2.0" - filter-obj "^1.1.0" split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -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== - -readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.2.2: + 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" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== dependencies: picomatch "^2.2.1" -redis-commands@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" - integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== - -redis-errors@^1.0.0, redis-errors@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= - -redis-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= dependencies: - redis-errors "^1.0.0" + indent-string "^2.1.0" + strip-indent "^1.0.1" -redis@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c" - integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw== +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== dependencies: - denque "^1.5.0" - redis-commands "^1.7.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" + extend-shallow "^3.0.2" + safe-regex "^1.1.0" -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== +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +regexpp@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.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@^1.0.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.0.0, resolve@^1.10.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: - is-core-module "^2.2.0" path-parse "^1.0.6" restore-cursor@^3.1.0: @@ -1764,15 +2019,17 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" -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== +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" rimraf@^2.6.1: version "2.7.1" @@ -1781,117 +2038,216 @@ rimraf@^2.6.1: dependencies: glob "^7.1.3" -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" +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -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== +rxjs@^6.6.0: + version "6.6.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" + integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== dependencies: - queue-microtask "^1.2.2" + tslib "^1.9.0" -rxjs@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68" - integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w== - dependencies: - tslib "~2.1.0" - -safe-buffer@^5.0.1, safe-buffer@~5.2.0: +safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= +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== -semver@^5.6.0: +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0: +semver@^6.1.2: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, 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== +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== dependencies: - lru-cache "^6.0.0" + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -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== +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= dependencies: - shebang-regex "^3.0.0" + shebang-regex "^1.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== +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.6" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" - integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" source-map-support@^0.5.12, source-map-support@^0.5.17: - 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== + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + source-map@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== + split-on-first@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^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= + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.5.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" @@ -1902,86 +2258,79 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= -string-argv@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" - integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - 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== +string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" + strip-ansi "^6.0.0" -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@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= -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== +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: - get-own-enumerable-property-symbols "^3.0.0" - is-obj "^1.0.1" - is-regexp "^1.0.0" + safe-buffer "~5.1.0" -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= +strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: - ansi-regex "^2.0.0" + ansi-regex "^4.1.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== +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== dependencies: - ansi-regex "^5.0.1" + ansi-regex "^5.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" 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-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-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" strip-json-comments@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.0.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== -supports-color@8.1.1: - 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-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -1996,17 +2345,20 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -tar@^6.1.11: - version "6.1.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== +systeminformation@^4.27.11: + version "4.27.11" + resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.27.11.tgz#6dbe96e48091444f80dab6c05ee1901286826b60" + integrity sha512-U7bigXbOnsB8k1vNHS0Y13RCsRz5/UohiUmND+3mMUL6vfzrpbe/h4ZqewowB+B+tJNnmGFDj08Z8xGfYo45dQ== + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" tdigest@^0.1.1: version "0.1.1" @@ -2020,11 +2372,33 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@^2.3.8: +through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + 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" @@ -2032,22 +2406,38 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -ts-node-dev@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.8.tgz#95520d8ab9d45fffa854d6668e2f8f9286241066" - integrity sha512-Q/m3vEwzYwLZKmV6/0VlFxcZzVV/xcgOt+Tx/VjaaRHyiBcFlV0541yrT09QjzzCxlDZ34OzKjrFAynlmtflEg== +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +ts-node-dev@^1.0.0-pre.44: + version "1.0.0-pre.63" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.63.tgz#0e69df26cef35a728362d93348f13caa2cb2c512" + integrity sha512-KURricXsXtiB4R+NCgiKgE01wyTe/GlXTdAPIhliDhF3kCn00kzyepAc1H8kbUJCmz0oYQq/GQ6CMtiWovs9qg== dependencies: - chokidar "^3.5.1" + chokidar "^3.4.0" + dateformat "~1.0.4-1.2.3" dynamic-dedupe "^0.3.0" minimist "^1.2.5" mkdirp "^1.0.4" @@ -2055,16 +2445,15 @@ ts-node-dev@^1.1.8: rimraf "^2.6.1" source-map-support "^0.5.12" tree-kill "^1.2.2" - ts-node "^9.0.0" + ts-node "^8.10.2" tsconfig "^7.0.0" -ts-node@^9.0.0: - version "9.1.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" - integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== +ts-node@^8.10.2: + version "8.10.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== dependencies: arg "^4.1.0" - create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" source-map-support "^0.5.17" @@ -2080,180 +2469,157 @@ tsconfig@^7.0.0: strip-bom "^3.0.0" strip-json-comments "^2.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@^1.8.1, tslib@^1.9.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== -tslib@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== - -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== +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== 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== +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.2.1" + prelude-ls "~1.1.2" -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.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== -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-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -typescript@^4.5.4: - version "4.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" - integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +type-is@^1.6.4, type-is@~1.6.17: + 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@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typescript@^3.8.3: + version "3.9.7" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== uWebSockets.js@uNetworking/uWebSockets.js#v18.5.0: version "18.5.0" resolved "https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/9b1605d2db82981cafe69dbe356e10ce412f5805" +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + 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== + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== dependencies: punycode "^2.1.0" -util-deprecate@^1.0.1: +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +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= -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== +uuid@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== uuidv4@*, uuidv4@^6.0.7: - version "6.2.12" - resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.12.tgz#e8c1d1d733c3fa4963d4610b8a3a09b4ec58ca96" - integrity sha512-UnN4ThIYWhv3ZUE8UwDnnCvh4JafCNu+sQkxmLyjCVwK3rjLfkg3DYiEv6oCMDIAIVEDP4INg4kX/C5hKaRzZA== + version "6.2.3" + resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.3.tgz#b478932d508484fda8a6a964fe2b897cca5eede2" + integrity sha512-4hxGisl76Y6A7nkadg5gMrPGVYVGLmJ3fZHVvmnXsy+8DMA7n7YV/4Y72Fw38CCwpZpyPgOaa/4YxhkCYwyNNQ== dependencies: - "@types/uuid" "8.3.1" - uuid "8.3.2" + "@types/uuid" "8.3.0" + uuid "8.3.0" 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== + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.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== +which@^1.2.9: + 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" -wide-align@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - -window-size@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" - integrity sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY= - -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== -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -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@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" - integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== - -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: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs@^3.10.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" - integrity sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU= - dependencies: - camelcase "^2.0.1" - cliui "^3.0.3" - decamelize "^1.1.1" - os-locale "^1.4.0" - string-width "^1.0.1" - window-size "^0.1.4" - y18n "^3.2.0" - yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" diff --git a/benchmark/index.ts b/benchmark/index.ts index 36d2c6f4..7be65cb7 100644 --- a/benchmark/index.ts +++ b/benchmark/index.ts @@ -2,7 +2,6 @@ import {RoomConnection} from "../front/src/Connexion/RoomConnection"; import {connectionManager} from "../front/src/Connexion/ConnectionManager"; import * as WebSocket from "ws" -let userMovedCount = 0; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -13,7 +12,8 @@ RoomConnection.setWebsocketFactory((url: string) => { }); async function startOneUser(): Promise { - const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'], + await connectionManager.anonymousLogin(true); + const connection = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'], { x: 783, y: 170 @@ -22,13 +22,7 @@ async function startOneUser(): Promise { bottom: 200, left: 500, right: 800 - }, null); - - const connection = onConnect.connection; - - connection.onUserMoved(() => { - userMovedCount++; - }) + }); console.log(connection.getUserId()); @@ -57,15 +51,10 @@ async function startOneUser(): Promise { (async () => { connectionManager.initBenchmark(); - const promises = []; for (let userNo = 0; userNo < 160; userNo++) { - const promise = startOneUser(); - promises.push(promise); + startOneUser(); // Wait 0.5s between adding users await sleep(125); } - - await Promise.all(promises); - console.log('User moved count: '+userMovedCount); })(); diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index 5d9ef0c6..8d4db6cf 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -209,9 +209,9 @@ } }, "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "requires": { "is-glob": "^4.0.1" } @@ -230,9 +230,9 @@ } }, "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, "indent-string": { "version": "2.1.0", @@ -429,9 +429,9 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-type": { "version": "1.1.0", @@ -688,9 +688,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" }, "xtend": { "version": "4.0.2", diff --git a/benchmark/package.json b/benchmark/package.json index 586e97e6..b0cd6a23 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -24,7 +24,7 @@ "@types/ws": "^7.2.6", "ts-node-dev": "^1.0.0-pre.62", "typescript": "^4.0.2", - "ws": "^7.4.6" + "ws": "^7.3.1" }, "devDependencies": {} } diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index 92541451..d93e3667 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -148,8 +148,8 @@ get-stdin@^4.0.1: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" glob-parent@~5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" dependencies: is-glob "^4.0.1" @@ -169,8 +169,8 @@ graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" indent-string@^2.1.0: version "2.1.0" @@ -315,8 +315,8 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" path-parse@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" path-type@^1.0.0: version "1.1.0" @@ -515,9 +515,9 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -ws@^7.4.6: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" +ws@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" xtend@^4.0.0: version "4.0.2" diff --git a/contrib/docker/.env.prod.template b/contrib/docker/.env.prod.template deleted file mode 100644 index 5e9adc87..00000000 --- a/contrib/docker/.env.prod.template +++ /dev/null @@ -1,118 +0,0 @@ -# Security -# - -SECRET_KEY= -ADMIN_API_TOKEN= - -# -# Networking -# - -# The base domain -DOMAIN=workadventure.localhost - -# Subdomains -# MUST match the DOMAIN variable above -FRONT_HOST=front.workadventure.localhost -PUSHER_HOST=pusher.workadventure.localhost -BACK_HOST=api.workadventure.localhost -MAPS_HOST=maps.workadventure.localhost -ICON_HOST=icon.workadventure.localhost - -# SAAS admin panel -ADMIN_API_URL= - -# -# Basic configuration -# - -# The directory to store data in -DATA_DIR=./wa - -# The URL used by default, in the form: "/_/global/map/url.json" -START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json - -# If you want to have a contact page in your menu, -# you MUST set CONTACT_URL to the URL of the page that you want -CONTACT_URL= - -MAX_PER_GROUP=4 -MAX_USERNAME_LENGTH=8 -DISABLE_ANONYMOUS=false - -# The version of the docker image to use -# MUST uncomment "image" keys in the docker-compose file for it to be effective -VERSION=master - -TZ=Europe/Paris - -# -# Jitsi -# - -JITSI_URL=meet.jit.si -# If your Jitsi environment has authentication set up, -# you MUST set JITSI_PRIVATE_MODE to "true" -# and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret -JITSI_PRIVATE_MODE=false -JITSI_ISS= -SECRET_JITSI_KEY= - -# -# Turn/Stun -# - -# URL of the TURN server (needed to "punch a hole" through some networks for P2P connections) -TURN_SERVER= -TURN_USER= -TURN_PASSWORD= -# If your Turn server is configured to use the Turn REST API, you MUST put the shared auth secret here. -# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file. -# Keep empty if you are sharing hard coded / clear text credentials. -TURN_STATIC_AUTH_SECRET= -# URL of the STUN server -STUN_SERVER= - -# -# Certificate config -# - -# The email address used by Let's encrypt to send renewal warnings (compulsory) -ACME_EMAIL= - -# -# Additional app configs -# Configuration for apps which are not workadventure itself -# - -# openID -OPID_CLIENT_ID= -OPID_CLIENT_SECRET= -OPID_CLIENT_ISSUER= -OPID_CLIENT_REDIRECT_URL= -OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen -OPID_PROFILE_SCREEN_PROVIDER= - - -# -# Advanced configuration -# Generally does not need to be changed -# - -# Networking -HTTP_PORT=80 -HTTPS_PORT=443 - -# Workadventure settings -DISABLE_NOTIFICATIONS=false -SKIP_RENDER_OPTIMIZATIONS=false -STORE_VARIABLES_FOR_LOCAL_MAPS=true - -# Debugging options -DEBUG_MODE=false -LOG_LEVEL=WARN - -# Internal URLs -API_URL=back:50051 - -RESTART_POLICY=unless-stopped diff --git a/contrib/docker/docker-compose.dev.yaml b/contrib/docker/docker-compose.dev.yaml deleted file mode 100644 index 0c3fc5b7..00000000 --- a/contrib/docker/docker-compose.dev.yaml +++ /dev/null @@ -1,62 +0,0 @@ -version: "3.3" -services: - front-dev: - build: - context: ../.. - dockerfile: front/Dockerfile - #image: thecodingmachine/workadventure-front:master - environment: - LIVE_RELOAD: "true" - NODE_ENV: "develop" - DEBUG_MODE: "$DEBUG_MODE" - JITSI_URL: "$JITSI_URL" - JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" - PUSHER_URL: "https://pusher.${DOMAIN}" - ICON_URL: "https://icon.${DOMAIN}" - API_URL: "pusher.${DOMAIN}" - STUN_SERVER: "${STUN_SERVER}" - TURN_SERVER: "${TURN_SERVER}" - TURN_USER: "${TURN_USER}" - TURN_PASSWORD: "${TURN_PASSWORD}" - START_ROOM_URL: "${START_ROOM_URL}" - MAX_PER_GROUP: "$MAX_PER_GROUP" - MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH" - FALLBACK_LOCALE: "${DEFAULT_LOCALE}" - ports: - - "127.0.0.1:8011:80" - restart: unless-stopped - - pusher-dev: - build: - context: ../.. - dockerfile: pusher/Dockerfile - #image: thecodingmachine/workadventure-pusher:master - command: yarn run runprod - environment: - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_KEY: yourSecretKey - API_URL: back-dev:50051 - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS - FRONT_URL: https://play.${DOMAIN} - ports: - - "127.0.0.1:8012:8080" - restart: unless-stopped - - back-dev: - build: - context: ../.. - dockerfile: back/Dockerfile - #image: thecodingmachine/workadventure-back:master - command: yarn run runprod - environment: - NODE_ENV: develop - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" - ADMIN_API_URL: "$ADMIN_API_URL" - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS - TURN_STATIC_AUTH_SECRET: "$TURN_STATIC_AUTH_SECRET" - ports: - - "127.0.0.1:8013:8080" - restart: unless-stopped diff --git a/contrib/docker/docker-compose.prod.yaml b/contrib/docker/docker-compose.prod.yaml deleted file mode 100644 index 80ed192b..00000000 --- a/contrib/docker/docker-compose.prod.yaml +++ /dev/null @@ -1,128 +0,0 @@ -version: "3.5" -services: - reverse-proxy: - image: traefik:v2.6 - command: - - --log.level=${LOG_LEVEL} - - --providers.docker - # Entry points - - --entryPoints.web.address=:${HTTP_PORT} - - --entrypoints.web.http.redirections.entryPoint.to=websecure - - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entryPoints.websecure.address=:${HTTPS_PORT} - # HTTP challenge - - --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL} - - --certificatesresolvers.myresolver.acme.storage=/acme.json - - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web - # Let's Encrypt's staging server - # uncomment during testing to avoid rate limiting - #- --certificatesresolvers.dnsresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory - ports: - - "${HTTP_PORT}:80" - - "${HTTPS_PORT}:443" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ${DATA_DIR}/letsencrypt/acme.json:/acme.json - restart: ${RESTART_POLICY} - - - front: - build: - context: ../.. - dockerfile: front/Dockerfile - #image: thecodingmachine/workadventure-front:${VERSION} - environment: - - DEBUG_MODE - - JITSI_URL - - JITSI_PRIVATE_MODE - - PUSHER_URL=//${PUSHER_HOST} - - ICON_URL=//${ICON_HOST} - - TURN_SERVER - - TURN_USER - - TURN_PASSWORD - - TURN_STATIC_AUTH_SECRET - - STUN_SERVER - - START_ROOM_URL - - SKIP_RENDER_OPTIMIZATIONS - - MAX_PER_GROUP - - MAX_USERNAME_LENGTH - - DISABLE_ANONYMOUS - - DISABLE_NOTIFICATIONS - labels: - - "traefik.http.routers.front.rule=Host(`${FRONT_HOST}`)" - - "traefik.http.routers.front.entryPoints=web" - - "traefik.http.services.front.loadbalancer.server.port=80" - - "traefik.http.routers.front-ssl.rule=Host(`${FRONT_HOST}`)" - - "traefik.http.routers.front-ssl.entryPoints=websecure" - - "traefik.http.routers.front-ssl.service=front" - - "traefik.http.routers.front-ssl.tls=true" - - "traefik.http.routers.front-ssl.tls.certresolver=myresolver" - restart: ${RESTART_POLICY} - - pusher: - build: - context: ../.. - dockerfile: pusher/Dockerfile - #image: thecodingmachine/workadventure-pusher:${VERSION} - command: yarn run runprod - environment: - - SECRET_JITSI_KEY - - SECRET_KEY - - API_URL - - FRONT_URL=https://${FRONT_HOST} - - JITSI_URL - - JITSI_ISS - - DISABLE_ANONYMOUS - labels: - - "traefik.http.routers.pusher.rule=Host(`${PUSHER_HOST}`)" - - "traefik.http.routers.pusher.entryPoints=web" - - "traefik.http.services.pusher.loadbalancer.server.port=8080" - - "traefik.http.routers.pusher-ssl.rule=Host(${PUSHER_HOST}`)" - - "traefik.http.routers.pusher-ssl.entryPoints=websecure" - - "traefik.http.routers.pusher-ssl.service=pusher" - - "traefik.http.routers.pusher-ssl.tls=true" - - "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver" - restart: ${RESTART_POLICY} - - back: - build: - context: ../.. - dockerfile: back/Dockerfile - #image: thecodingmachine/workadventure-back:${VERSION} - command: yarn run runprod - environment: - - SECRET_JITSI_KEY - - SECRET_KEY - - ADMIN_API_TOKEN - - ADMIN_API_URL - - TURN_SERVER - - TURN_USER - - TURN_PASSWORD - - TURN_STATIC_AUTH_SECRET - - STUN_SERVER - - JITSI_URL - - JITSI_ISS - - MAX_PER_GROUP - - STORE_VARIABLES_FOR_LOCAL_MAPS - labels: - - "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)" - - "traefik.http.routers.back.entryPoints=web" - - "traefik.http.services.back.loadbalancer.server.port=8080" - - "traefik.http.routers.back-ssl.rule=Host(`${BACK_HOST}`)" - - "traefik.http.routers.back-ssl.entryPoints=websecure" - - "traefik.http.routers.back-ssl.service=back" - - "traefik.http.routers.back-ssl.tls=true" - - "traefik.http.routers.back-ssl.tls.certresolver=myresolver" - restart: ${RESTART_POLICY} - - icon: - image: matthiasluedtke/iconserver:v3.13.0 - labels: - - "traefik.http.routers.icon.rule=Host(`${ICON_HOST}`)" - - "traefik.http.routers.icon.entryPoints=web,traefik" - - "traefik.http.services.icon.loadbalancer.server.port=8080" - - "traefik.http.routers.icon-ssl.rule=Host(`${ICON_HOST}`)" - - "traefik.http.routers.icon-ssl.entryPoints=websecure" - - "traefik.http.routers.icon-ssl.service=icon" - - "traefik.http.routers.icon-ssl.tls=true" - - "traefik.http.routers.icon-ssl.tls.certresolver=myresolver" diff --git a/contrib/docker/docker-compose.yaml b/contrib/docker/docker-compose.yaml deleted file mode 100644 index e9495393..00000000 --- a/contrib/docker/docker-compose.yaml +++ /dev/null @@ -1,63 +0,0 @@ -version: "3.3" -services: - front: - build: - context: ../.. - dockerfile: front/Dockerfile - #image: thecodingmachine/workadventure-front:master - environment: - #LIVE_RELOAD: "true" - NODE_ENV: "production" - DEBUG_MODE: "$DEBUG_MODE" - JITSI_URL: "$JITSI_URL" - JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" - PUSHER_URL: "https://pusher.${DOMAIN}" - ICON_URL: "https://icon.${DOMAIN}" - API_URL: "pusher.${DOMAIN}" - STUN_SERVER: "${STUN_SERVER}" - TURN_SERVER: "${TURN_SERVER}" - TURN_USER: "${TURN_USER}" - TURN_PASSWORD: "${TURN_PASSWORD}" - START_ROOM_URL: "${START_ROOM_URL}" - MAX_PER_GROUP: "$MAX_PER_GROUP" - MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH" - FALLBACK_LOCALE: "${DEFAULT_LOCALE}" - ports: - - "127.0.0.1:8001:80" - restart: unless-stopped - - pusher: - build: - context: ../.. - dockerfile: pusher/Dockerfile - #image: thecodingmachine/workadventure-pusher:master - command: yarn run runprod - environment: - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_KEY: yourSecretKey - API_URL: back:50051 - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS - FRONT_URL: https://play.${DOMAIN} - ports: - - "127.0.0.1:8002:8080" - restart: unless-stopped - - back: - build: - context: ../.. - dockerfile: back/Dockerfile - #image: thecodingmachine/workadventure-back:master - command: yarn run runprod - environment: - NODE_ENV: production - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" - ADMIN_API_URL: "$ADMIN_API_URL" - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS - TURN_STATIC_AUTH_SECRET: "$TURN_STATIC_AUTH_SECRET" - ports: - - "127.0.0.1:8003:8080" - restart: unless-stopped - diff --git a/contrib/nginx.conf b/contrib/nginx.conf deleted file mode 100644 index eb17bbcb..00000000 --- a/contrib/nginx.conf +++ /dev/null @@ -1,42 +0,0 @@ -# vim: syntax=conf - -map $http_host $krautworld_upstream { - hostnames; - default http://127.0.0.1:8000; - - icon.kraut.world http://127.0.0.1:7999; - - play.kraut.world http://127.0.0.1:8001; - pusher.kraut.world http://127.0.0.1:8002; - api.kraut.world http://127.0.0.1:8003; - maps.kraut.world http://127.0.0.1:8004; - - play.dev.kraut.world http://127.0.0.1:8011; - pusher.dev.kraut.world http://127.0.0.1:8012; - api.dev.kraut.world http://127.0.0.1:8013; - maps.dev.kraut.world http://127.0.0.1:8014; -} - -server { - listen 127.0.0.1:8443 ssl http2; - listen [::1]:8443 ssl http2; - server_name .kraut.world .dev.kraut.world; - - ssl_certificate /var/lib/dehydrated/certs/play.kraut.world/fullchain.pem; - ssl_certificate_key /var/lib/dehydrated/certs/play.kraut.world/privkey.pem; - - set $HSTS_header "max-age=16000000"; - - location / { - proxy_pass $krautworld_upstream; - - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - proxy_pass_header Set-Cookie; - } -} diff --git a/contrib/systemd/workadventure-icon.service b/contrib/systemd/workadventure-icon.service deleted file mode 100644 index 43d556e3..00000000 --- a/contrib/systemd/workadventure-icon.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Workadventure icon service -Requires=docker.service -After=docker.service - -[Service] -ExecStart=/usr/bin/docker run --rm -p "127.0.0.1:7999:8080" --name icon matthiasluedtke/iconserver:v3.13.0 -ExecStopPost=/usr/bin/docker rm -f icon - -[Install] -WantedBy=multi-user.target diff --git a/deeployer.libsonnet b/deeployer.libsonnet index 4012b186..4edb4728 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -1,129 +1,71 @@ { local env = std.extVar("env"), - local namespace = env.DEPLOY_REF, + local namespace = env.GITHUB_REF_SLUG, local tag = namespace, - local url = namespace+".test.workadventu.re", - // develop branch does not use admin because of issue with SSL certificate of admin as of now. - local adminUrl = if std.startsWith(namespace, "admin") then "https://"+url else null, + local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com", "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", "version": "1.0", "containers": { - "back1": { + "back": { "image": "thecodingmachine/workadventure-back:"+tag, "host": { - "url": "api1-"+url, - "containerPort": 8080 + "url": "api."+url, + "https": "enable" }, - "ports": [8080, 50051], + "ports": [8080], "env": { "SECRET_KEY": "tempSecretKeyNeedsToChange", + "ADMIN_API_TOKEN": env.ADMIN_API_TOKEN, + "ADMIN_API_URL": "https://admin."+url, "JITSI_ISS": env.JITSI_ISS, "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, - "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET, - "REDIS_HOST": "redis", - } + (if adminUrl != null then { - "ADMIN_API_URL": adminUrl, - "ADMIN_API_TOKEN": env.ADMIN_API_TOKEN, - } else {}) + } }, - "back2": { - "image": "thecodingmachine/workadventure-back:"+tag, - "host": { - "url": "api2-"+url, - "containerPort": 8080 - }, - "ports": [8080, 50051], - "env": { - "SECRET_KEY": "tempSecretKeyNeedsToChange", - "JITSI_ISS": env.JITSI_ISS, - "JITSI_URL": env.JITSI_URL, - "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, - "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET, - "REDIS_HOST": "redis", - } + (if adminUrl != null then { - "ADMIN_API_URL": adminUrl, - "ADMIN_API_TOKEN": env.ADMIN_API_TOKEN, - } else {}) - }, - "pusher": { - "replicas": 2, - "image": "thecodingmachine/workadventure-pusher:"+tag, - "host": { - "url": "pusher-"+url, - }, - "ports": [8080], - "env": { - "SECRET_KEY": "tempSecretKeyNeedsToChange", - "JITSI_ISS": env.JITSI_ISS, - "JITSI_URL": env.JITSI_URL, - "API_URL": "back1:50051,back2:50051", - "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, - "FRONT_URL": "https://play-"+url - } + (if adminUrl != null then { - "ADMIN_API_URL": adminUrl, - "ADMIN_API_TOKEN": env.ADMIN_API_TOKEN, - "ADMIN_SOCKETS_TOKEN": env.ADMIN_SOCKETS_TOKEN, - } else {}) - }, "front": { "image": "thecodingmachine/workadventure-front:"+tag, "host": { - "url": "play-"+url, + "url": "play."+url, + "https": "enable" }, "ports": [80], "env": { - "PUSHER_URL": "//pusher-"+url, - "UPLOADER_URL": "//uploader-"+url, - "ADMIN_URL": "//"+url, + "API_URL": "api."+url, "JITSI_URL": env.JITSI_URL, - #POSTHOG - "POSTHOG_API_KEY": if namespace == "master" then env.POSTHOG_API_KEY else "", - "POSTHOG_URL": if namespace == "master" then env.POSTHOG_URL else "", "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", - "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", - "START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json", - "ICON_URL": "//icon-"+url, + "TURN_USER": "workadventure", + "TURN_PASSWORD": "WorkAdventure123", + "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false" } }, - "uploader": { - "image": "thecodingmachine/workadventure-uploader:"+tag, - "host": { - "url": "uploader-"+url, - "containerPort": 8080 - }, - "ports": [8080], - "env": { - } - }, "maps": { "image": "thecodingmachine/workadventure-maps:"+tag, "host": { - "url": "maps-"+url + "url": "maps."+url, + "https": "enable" + }, + "ports": [80] + }, + "website": { + "image": "thecodingmachine/workadventure-website:"+tag, + "host": { + "url": url, + "https": "enable" }, "ports": [80], "env": { - "FRONT_URL": "https://play-"+url + "GAME_URL": "https://play."+url } - }, - "redis": { - "image": "redis:6", - "ports": [6379] - }, - "iconserver": { - "image": "matthiasluedtke/iconserver:v3.13.0", - "host": { - "url": "icon-"+url, - "containerPort": 8080, - }, - "ports": [8080] - }, + } }, "config": { + "https": { + "mail": "d.negrier@thecodingmachine.com" + }, k8sextension(k8sConf):: k8sConf + { - back1+: { + back+: { deployment+: { spec+: { template+: { @@ -135,100 +77,8 @@ } } } - }, - ingress+: { - spec+: { - tls+: [{ - hosts: ["api1-"+url], - secretName: "certificate-tls" - }] - } } - }, - back2+: { - deployment+: { - spec+: { - template+: { - metadata+: { - annotations+: { - "prometheus.io/port": "8080", - "prometheus.io/scrape": "true" - } - } - } - } - }, - ingress+: { - spec+: { - tls+: [{ - hosts: ["api2-"+url], - secretName: "certificate-tls" - }] - } - } - }, - pusher+: { - deployment+: { - spec+: { - template+: { - metadata+: { - annotations+: { - "prometheus.io/port": "8080", - "prometheus.io/scrape": "true" - } - } - } - } - }, - ingress+: { - spec+: { - tls+: [{ - hosts: ["pusher-"+url], - secretName: "certificate-tls" - }] - } - } - }, - front+: { - ingress+: { - spec+: { - tls+: [{ - hosts: ["play-"+url], - secretName: "certificate-tls" - }] - } - } - }, - uploader+: { - ingress+: { - spec+: { - tls+: [{ - hosts: ["uploader-"+url], - secretName: "certificate-tls" - }] - } - } - }, - maps+: { - ingress+: { - spec+: { - tls+: [{ - hosts: ["maps-"+url], - secretName: "certificate-tls" - }] - } - } - }, - iconserver+: { - ingress+: { - spec+: { - tls+: [{ - hosts: ["icon-"+url], - secretName: "certificate-tls" - }] - } - } - }, + } } } } diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 00000000..bdf5855a --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,20 @@ +version: '3' + +services: + + wait_app: + image: dadarek/wait-for-dependencies + depends_on: + - reverse-proxy + command: front:8080 + cypress: + # the Docker image to use from https://github.com/cypress-io/cypress-docker-images + image: "cypress/included:3.8.3" + depends_on: + - reverse-proxy + environment: + # pass base url to test pointing at the web application + - CYPRESS_baseUrl=http://front:8080 + working_dir: /e2e + volumes: + - ./e2e/:/e2e \ No newline at end of file diff --git a/docker-compose.single-domain.yaml b/docker-compose.single-domain.yaml deleted file mode 100644 index 1612e396..00000000 --- a/docker-compose.single-domain.yaml +++ /dev/null @@ -1,227 +0,0 @@ -version: "3" -services: - reverse-proxy: - image: traefik:v2.0 - command: - - --api.insecure=true - - --providers.docker - - --entryPoints.web.address=:80 - - --entryPoints.websecure.address=:443 - ports: - - "80:80" - - "443:443" - # The Web UI (enabled by --api.insecure=true) - - "8080:8080" - depends_on: - - back - - front - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - front: - image: thecodingmachine/nodejs:14 - environment: - DEBUG_MODE: "$DEBUG_MODE" - JITSI_URL: $JITSI_URL - JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" - HOST: "0.0.0.0" - NODE_ENV: development - PUSHER_URL: /pusher - UPLOADER_URL: /uploader - #ADMIN_URL: /admin - MAPS_URL: /maps - ICON_URL: /icon - STARTUP_COMMAND_1: ./templater.sh - STARTUP_COMMAND_2: yarn install - TURN_SERVER: "turn:localhost:3478,turns:localhost:5349" - DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS" - SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS" - # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials. - # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container - TURN_USER: "" - TURN_PASSWORD: "" - START_ROOM_URL: "$START_ROOM_URL" - DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS" - command: yarn run start - volumes: - - ./front:/usr/src/app - labels: - - "traefik.http.routers.front.rule=PathPrefix(`/`)" - - "traefik.http.routers.front.entryPoints=web,traefik" - - "traefik.http.services.front.loadbalancer.server.port=8080" - - "traefik.http.routers.front-ssl.rule=PathPrefix(`/`)" - - "traefik.http.routers.front-ssl.entryPoints=websecure" - - "traefik.http.routers.front-ssl.tls=true" - - "traefik.http.routers.front-ssl.service=front" - - pusher: - image: thecodingmachine/nodejs:14 - command: yarn dev - #command: yarn run prod - #command: yarn run profile - environment: - DEBUG: "*" - STARTUP_COMMAND_1: yarn install - # wait for files generated by "messages" container to exists - STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_KEY: yourSecretKey - ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" - API_URL: back:50051 - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS - FRONT_URL: http://localhost - OPID_CLIENT_ID: $OPID_CLIENT_ID - OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET - OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER - OPID_CLIENT_REDIRECT_URL: $OPID_CLIENT_REDIRECT_URL - OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER - DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS - volumes: - - ./pusher:/usr/src/app - labels: - - "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher" - - "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)" - - "traefik.http.routers.pusher.middlewares=strip-pusher-prefix@docker" - - "traefik.http.routers.pusher.entryPoints=web" - - "traefik.http.services.pusher.loadbalancer.server.port=8080" - - "traefik.http.routers.pusher-ssl.rule=PathPrefix(`/pusher`)" - - "traefik.http.routers.pusher-ssl.middlewares=strip-pusher-prefix@docker" - - "traefik.http.routers.pusher-ssl.entryPoints=websecure" - - "traefik.http.routers.pusher-ssl.tls=true" - - "traefik.http.routers.pusher-ssl.service=pusher" - - maps: - image: thecodingmachine/php:8.1-v4-apache-node12 - environment: - DEBUG_MODE: "$DEBUG_MODE" - HOST: "0.0.0.0" - NODE_ENV: development - FRONT_URL: http://play.workadventure.localhost - #APACHE_DOCUMENT_ROOT: dist/ - #APACHE_EXTENSIONS: headers - #APACHE_EXTENSION_HEADERS: 1 - STARTUP_COMMAND_0: sudo a2enmod headers - STARTUP_COMMAND_1: yarn install - STARTUP_COMMAND_2: yarn run dev & - volumes: - - ./maps:/var/www/html - labels: - - "traefik.http.middlewares.strip-maps-prefix.stripprefix.prefixes=/maps" - - "traefik.http.routers.maps.rule=PathPrefix(`/maps`)" - - "traefik.http.routers.maps.middlewares=strip-maps-prefix@docker" - - "traefik.http.routers.maps.entryPoints=web,traefik" - - "traefik.http.services.maps.loadbalancer.server.port=80" - - "traefik.http.routers.maps-ssl.rule=PathPrefix(`/maps`)" - - "traefik.http.routers.maps-ssl.middlewares=strip-maps-prefix@docker" - - "traefik.http.routers.maps-ssl.entryPoints=websecure" - - "traefik.http.routers.maps-ssl.tls=true" - - "traefik.http.routers.maps-ssl.service=maps" - - back: - image: thecodingmachine/nodejs:12 - command: yarn dev - #command: yarn run profile - environment: - DEBUG: "*" - STARTUP_COMMAND_1: yarn install - # wait for files generated by "messages" container to exists - STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done - SECRET_KEY: yourSecretKey - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - ALLOW_ARTILLERY: "true" - ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS - MAX_PER_GROUP: "$MAX_PER_GROUP" - REDIS_HOST: redis - NODE_ENV: development - volumes: - - ./back:/usr/src/app - labels: - - "traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api" - - "traefik.http.routers.back.rule=PathPrefix(`/api`)" - - "traefik.http.routers.back.middlewares=strip-api-prefix@docker" - - "traefik.http.routers.back.entryPoints=web" - - "traefik.http.services.back.loadbalancer.server.port=8080" - - "traefik.http.routers.back-ssl.rule=PathPrefix(`/api`)" - - "traefik.http.routers.back-ssl.middlewares=strip-api-prefix@docker" - - "traefik.http.routers.back-ssl.entryPoints=websecure" - - "traefik.http.routers.back-ssl.tls=true" - - "traefik.http.routers.back-ssl.service=back" - - uploader: - image: thecodingmachine/nodejs:12 - command: yarn dev - #command: yarn run profile - environment: - DEBUG: "*" - STARTUP_COMMAND_1: yarn install - volumes: - - ./uploader:/usr/src/app - labels: - - "traefik.http.middlewares.strip-uploader-prefix.stripprefix.prefixes=/uploader" - - "traefik.http.routers.uploader.rule=PathPrefix(`/uploader`)" - - "traefik.http.routers.uploader.middlewares=strip-uploader-prefix@docker" - - "traefik.http.routers.uploader.entryPoints=web" - - "traefik.http.services.uploader.loadbalancer.server.port=8080" - - "traefik.http.routers.uploader-ssl.rule=PathPrefix(`/uploader`)" - - "traefik.http.routers.uploader-ssl.middlewares=strip-uploader-prefix@docker" - - "traefik.http.routers.uploader-ssl.entryPoints=websecure" - - "traefik.http.routers.uploader-ssl.tls=true" - - "traefik.http.routers.uploader-ssl.service=uploader" - - messages: - #image: thecodingmachine/nodejs:14 - image: thecodingmachine/workadventure-back-base:latest - environment: - #STARTUP_COMMAND_0: sudo apt-get install -y inotify-tools - STARTUP_COMMAND_1: yarn install - STARTUP_COMMAND_2: yarn run proto:watch - volumes: - - ./messages:/usr/src/app - - ./back:/usr/src/back - - ./front:/usr/src/front - - ./pusher:/usr/src/pusher - - redis: - image: redis:6 - - icon: - image: matthiasluedtke/iconserver:v3.13.0 - labels: - - "traefik.http.middlewares.strip-icon-prefix.stripprefix.prefixes=/icon" - - "traefik.http.routers.icon.rule=PathPrefix(`/icon`)" - - "traefik.http.routers.icon.middlewares=strip-icon-prefix@docker" - - "traefik.http.routers.icon.entryPoints=web" - - "traefik.http.services.icon.loadbalancer.server.port=8080" - - "traefik.http.routers.icon-ssl.rule=PathPrefix(`/icon`)" - - "traefik.http.routers.icon-ssl.middlewares=strip-icon-prefix@docker" - - "traefik.http.routers.icon-ssl.entryPoints=websecure" - - "traefik.http.routers.icon-ssl.tls=true" - - "traefik.http.routers.icon-ssl.service=icon" - -# coturn: -# image: coturn/coturn:4.5.2 -# command: -# - turnserver -# #- -c=/etc/coturn/turnserver.conf -# - --log-file=stdout -# - --external-ip=$$(detect-external-ip) -# - --listening-port=3478 -# - --min-port=10000 -# - --max-port=10010 -# - --tls-listening-port=5349 -# - --listening-ip=0.0.0.0 -# - --realm=localhost -# - --server-name=localhost -# - --lt-cred-mech -# # Enable Coturn "REST API" to validate temporary passwords. -# #- --use-auth-secret -# #- --static-auth-secret=SomeStaticAuthSecret -# #- --userdb=/var/lib/turn/turndb -# - --user=workadventure:WorkAdventure123 -# # use real-valid certificate/privatekey files -# #- --cert=/root/letsencrypt/fullchain.pem -# #- --pkey=/root/letsencrypt/privkey.pem -# network_mode: host diff --git a/docker-compose.testcafe.yml b/docker-compose.testcafe.yml deleted file mode 100644 index e61db21d..00000000 --- a/docker-compose.testcafe.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: "3.5" -services: - testcafe: - build: tests/ - working_dir: /project/tests - command: - - --dev - # Run as root to have the right to access /var/run/docker.sock - user: root - environment: - BROWSER: "chromium --use-fake-device-for-media-stream" - PROJECT_DIR: ${PROJECT_DIR} - ADMIN_API_TOKEN: ${ADMIN_API_TOKEN} - volumes: - - ./:/project - - ./maps:/maps - - /var/run/docker.sock:/var/run/docker.sock - # security_opt: - # - seccomp:unconfined diff --git a/docker-compose.yaml b/docker-compose.yaml index 8489b336..482dfbcb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,29 +1,22 @@ -version: "3.5" +version: "3" services: reverse-proxy: - image: traefik:v2.5 + image: traefik:v2.0 command: - --api.insecure=true - --providers.docker - --entryPoints.web.address=:80 - --entryPoints.websecure.address=:443 - - "--providers.docker.exposedbydefault=false" ports: - "80:80" - "443:443" # The Web UI (enabled by --api.insecure=true) - "8080:8080" - #depends_on: - # - back - # - front + depends_on: + - back + - front volumes: - /var/run/docker.sock:/var/run/docker.sock - networks: - default: - aliases: - - 'play.workadventure.localhost' - - 'pusher.workadventure.localhost' - - 'maps.workadventure.localhost' front: image: thecodingmachine/nodejs:14 @@ -33,79 +26,29 @@ services: JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" HOST: "0.0.0.0" NODE_ENV: development - PUSHER_URL: //pusher.workadventure.localhost - UPLOADER_URL: //uploader.workadventure.localhost - #ADMIN_URL: //workadventure.localhost - ICON_URL: //icon.workadventure.localhost - STARTUP_COMMAND_1: ./templater.sh - STARTUP_COMMAND_2: yarn install - STUN_SERVER: "stun:stun.l.google.com:19302" - TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349" - DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS" - SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS" - # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials. - # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container - TURN_USER: "" - TURN_PASSWORD: "" - START_ROOM_URL: "$START_ROOM_URL" - MAX_PER_GROUP: "$MAX_PER_GROUP" - MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH" - DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS" - OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER" - LIVE_RELOAD: "$LIVE_RELOAD:-true" + API_URL: api.workadventure.localhost + STARTUP_COMMAND_1: yarn install + TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443" + TURN_USER: workadventure + TURN_PASSWORD: WorkAdventure123 command: yarn run start volumes: - ./front:/usr/src/app labels: - - "traefik.enable=true" - "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)" - - "traefik.http.routers.front.entryPoints=web" + - "traefik.http.routers.front.entryPoints=web,traefik" - "traefik.http.services.front.loadbalancer.server.port=8080" - "traefik.http.routers.front-ssl.rule=Host(`play.workadventure.localhost`)" - "traefik.http.routers.front-ssl.entryPoints=websecure" - "traefik.http.routers.front-ssl.tls=true" - "traefik.http.routers.front-ssl.service=front" - pusher: - image: thecodingmachine/nodejs:14 - command: yarn dev - environment: - DEBUG: "socket:*" - STARTUP_COMMAND_1: yarn install - # wait for files generated by "messages" container to exists - STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_KEY: yourSecretKey - ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" - API_URL: back:50051 - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS - FRONT_URL: http://play.workadventure.localhost - OPID_CLIENT_ID: $OPID_CLIENT_ID - OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET - OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER - OPID_CLIENT_REDIRECT_URL: $OPID_CLIENT_REDIRECT_URL - OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER - DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS - volumes: - - ./pusher:/usr/src/app - labels: - - "traefik.enable=true" - - "traefik.http.routers.pusher.rule=Host(`pusher.workadventure.localhost`)" - - "traefik.http.routers.pusher.entryPoints=web" - - "traefik.http.services.pusher.loadbalancer.server.port=8080" - - "traefik.http.routers.pusher-ssl.rule=Host(`pusher.workadventure.localhost`)" - - "traefik.http.routers.pusher-ssl.entryPoints=websecure" - - "traefik.http.routers.pusher-ssl.tls=true" - - "traefik.http.routers.pusher-ssl.service=pusher" - maps: - image: thecodingmachine/php:8.1-v4-apache-node12 + image: thecodingmachine/nodejs:12-apache environment: DEBUG_MODE: "$DEBUG_MODE" HOST: "0.0.0.0" NODE_ENV: development - FRONT_URL: http://play.workadventure.localhost #APACHE_DOCUMENT_ROOT: dist/ #APACHE_EXTENSIONS: headers #APACHE_EXTENSION_HEADERS: 1 @@ -115,7 +58,6 @@ services: volumes: - ./maps:/var/www/html labels: - - "traefik.enable=true" - "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)" - "traefik.http.routers.maps.entryPoints=web,traefik" - "traefik.http.services.maps.loadbalancer.server.port=80" @@ -129,25 +71,16 @@ services: command: yarn dev #command: yarn run profile environment: - DEBUG: "*" STARTUP_COMMAND_1: yarn install - # wait for files generated by "messages" container to exists - STARTUP_COMMAND_2: sleep 5; while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done SECRET_KEY: yourSecretKey SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" ALLOW_ARTILLERY: "true" ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" JITSI_URL: $JITSI_URL JITSI_ISS: $JITSI_ISS - TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret - MAX_PER_GROUP: "MAX_PER_GROUP" - REDIS_HOST: redis - NODE_ENV: development - STORE_VARIABLES_FOR_LOCAL_MAPS: "true" volumes: - ./back:/usr/src/app labels: - - "traefik.enable=true" - "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)" - "traefik.http.routers.back.entryPoints=web" - "traefik.http.services.back.loadbalancer.server.port=8080" @@ -156,86 +89,30 @@ services: - "traefik.http.routers.back-ssl.tls=true" - "traefik.http.routers.back-ssl.service=back" - uploader: - image: thecodingmachine/nodejs:12 - command: yarn dev - #command: yarn run profile + + website: + image: thecodingmachine/nodejs:12-apache environment: - DEBUG: "*" - STARTUP_COMMAND_1: yarn install + STARTUP_COMMAND_1: npm install + STARTUP_COMMAND_2: npm run watch & + APACHE_DOCUMENT_ROOT: dist/ volumes: - - ./uploader:/usr/src/app + - ./website:/var/www/html labels: - - "traefik.enable=true" - - "traefik.http.routers.uploader.rule=Host(`uploader.workadventure.localhost`)" - - "traefik.http.routers.uploader.entryPoints=web" - - "traefik.http.services.uploader.loadbalancer.server.port=8080" - - "traefik.http.routers.uploader-ssl.rule=Host(`uploader.workadventure.localhost`)" - - "traefik.http.routers.uploader-ssl.entryPoints=websecure" - - "traefik.http.routers.uploader-ssl.tls=true" - - "traefik.http.routers.uploader-ssl.service=uploader" + - "traefik.http.routers.website.rule=Host(`workadventure.localhost`)" + - "traefik.http.routers.website.entryPoints=web" + - "traefik.http.services.website.loadbalancer.server.port=80" + - "traefik.http.routers.website-ssl.rule=Host(`workadventure.localhost`)" + - "traefik.http.routers.website-ssl.entryPoints=websecure" + - "traefik.http.routers.website-ssl.tls=true" + - "traefik.http.routers.website-ssl.service=website" messages: - #image: thecodingmachine/nodejs:14 image: thecodingmachine/workadventure-back-base:latest environment: - #STARTUP_COMMAND_0: sudo apt-get install -y inotify-tools STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_2: yarn run proto:watch volumes: - ./messages:/usr/src/app - ./back:/usr/src/back - ./front:/usr/src/front - - ./pusher:/usr/src/pusher - - redis: - image: redis:6 - - redisinsight: - image: redislabs/redisinsight:latest - labels: - - "traefik.enable=true" - - "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)" - - "traefik.http.routers.redisinsight.entryPoints=web" - - "traefik.http.services.redisinsight.loadbalancer.server.port=8001" - - "traefik.http.routers.redisinsight-ssl.rule=Host(`redis.workadventure.localhost`)" - - "traefik.http.routers.redisinsight-ssl.entryPoints=websecure" - - "traefik.http.routers.redisinsight-ssl.tls=true" - - "traefik.http.routers.redisinsight-ssl.service=redisinsight" - - icon: - image: matthiasluedtke/iconserver:v3.13.0 - labels: - - "traefik.enable=true" - - "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)" - - "traefik.http.routers.icon.entryPoints=web" - - "traefik.http.services.icon.loadbalancer.server.port=8080" - - "traefik.http.routers.icon-ssl.rule=Host(`icon.workadventure.localhost`)" - - "traefik.http.routers.icon-ssl.entryPoints=websecure" - - "traefik.http.routers.icon-ssl.tls=true" - - "traefik.http.routers.icon-ssl.service=icon" - -# coturn: -# image: coturn/coturn:4.5.2 -# command: -# - turnserver -# #- -c=/etc/coturn/turnserver.conf -# - --log-file=stdout -# - --external-ip=$$(detect-external-ip) -# - --listening-port=3478 -# - --min-port=10000 -# - --max-port=10010 -# - --tls-listening-port=5349 -# - --listening-ip=0.0.0.0 -# - --realm=coturn.workadventure.localhost -# - --server-name=coturn.workadventure.localhost -# - --lt-cred-mech -# # Enable Coturn "REST API" to validate temporary passwords. -# #- --use-auth-secret -# #- --static-auth-secret=SomeStaticAuthSecret -# #- --userdb=/var/lib/turn/turndb -# - --user=workadventure:WorkAdventure123 -# # use real-valid certificate/privatekey files -# #- --cert=/root/letsencrypt/fullchain.pem -# #- --pkey=/root/letsencrypt/privkey.pem -# network_mode: host diff --git a/docs/dev/README.md b/docs/dev/README.md deleted file mode 100644 index d05c4884..00000000 --- a/docs/dev/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Developer documentation - -This (work in progress) documentation provides a number of "how-to" guides explaining how to work on the WorkAdventure -code. - -This documentation is targeted at developers looking to open Pull Requests on WorkAdventure. - -If you "only" want to design dynamic maps, please refer instead to the [scripting API documentation](https://workadventu.re/map-building/scripting.md). - -## Contributing - -Check out the [contributing guide](../../CONTRIBUTING.md) - -## Front documentation - -- [How to add new functions in the scripting API](contributing-to-scripting-api.md) diff --git a/docs/dev/contributing-to-scripting-api.md b/docs/dev/contributing-to-scripting-api.md deleted file mode 100644 index 8d716010..00000000 --- a/docs/dev/contributing-to-scripting-api.md +++ /dev/null @@ -1,276 +0,0 @@ -# How to add new functions in the scripting API - -This documentation is intended at contributors who want to participate in the development of WorkAdventure itself. -Before reading this, please be sure you are familiar with the [scripting API](https://workadventu.re/map-building/scripting.md). - -The [scripting API](https://workadventu.re/map-building/scripting.md) allows map developers to add dynamic features in their maps. - -## Why extend the scripting API? - -The philosophy behind WorkAdventure is to build a platform that is as open as possible. Part of this strategy is to -offer map developers the ability to turn a WorkAdventures map into something unexpected, using the API. For instance, -you could use it to develop games (we have seen a PacMan and a mine-sweeper on WorkAdventure!) - -We started working on the WorkAdventure scripting API with this in mind, but at some point, maybe you will find that -a feature is missing in the API. This article is here to explain to you how to add this feature. - -## How to extend the scripting API? - -Extending the scripting API means modifying the core of WorkAdventure. You can of course run these -modifications on your self-hosted instance. -But if you want to share it with the wider community, I strongly encourage you to start by [opening an issue](https://github.com/thecodingmachine/workadventure/issues) -on GitHub before starting the development. Check with the core maintainers that they are willing to merge your idea -before starting developing it. Once a new function makes it into the scripting API, it is very difficult to make it -evolve (or to deprecate), so the design of the function you add needs to be carefully considered. - -## How does it work? - -Scripts are executed in the browser, inside an iframe. - -![](images/scripting_1.svg) - -The iframe allows WorkAdventure to isolate the script in a sandbox. Because the iframe is sandbox (or on a different -domain than the WorkAdventure server), scripts cannot directly manipulate the DOM of WorkAdventure. They also cannot -directly access Phaser objects (Phaser is the game engine used in WorkAdventure). This is by-design. Since anyone -can contribute a map, we cannot allow anyone to run any code in the scope of the WorkAdventure server (that would be -a huge XSS security flaw). - -Instead, the only way the script can interact with WorkAdventure is by sending messages using the -[postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). - -![](images/scripting_2.svg) - -We want to make life easy for map developers. So instead of asking them to directly send messages using the postMessage -API, we provide a nice library that does this work for them. This library is what we call the "Scripting API" (we sometimes -refer to it as the "Client API"). - -The scripting API provides the global `WA` object. - -## A simple example - -So let's take an example with a sample script: - -```typescript -WA.chat.sendChatMessage('Hello world!', 'John Doe'); -``` - -When this script is called, the scripting API is dispatching a JSON message to WorkAdventure. - -In our case, the `sendChatMessage` function looks like this: - -**src/Api/iframe/chat.ts** -```typescript - sendChatMessage(message: string, author: string) { - sendToWorkadventure({ - type: "chat", - data: { - message: message, - author: author, - }, - }); - } -``` - -The `sendToWorkadventure` function is a utility function that dispatches the message to the main frame. - -In WorkAdventure, the message is received in the [`IframeListener` listener class](http://github.com/thecodingmachine/workadventure/blob/1e6ce4dec8697340e2c91798864b94da9528b482/front/src/Api/IframeListener.ts#L200-L203). -This class is in charge of analyzing the JSON messages received and dispatching them to the right place in the WorkAdventure application. - -The message callback implemented in `IframeListener` is a giant (and disgusting) `if` statement branching to the correct -part of the code depending on the `type` property. - -**src/Api/IframeListener.ts** -```typescript -// ... - } else if (payload.type === "setProperty" && isSetPropertyEvent(payload.data)) { - this._setPropertyStream.next(payload.data); - } else if (payload.type === "chat" && isChatEvent(payload.data)) { - scriptUtils.sendAnonymousChat(payload.data); - } else if (payload.type === "openPopup" && isOpenPopupEvent(payload.data)) { - this._openPopupStream.next(payload.data); - } else if (payload.type === "closePopup" && isClosePopupEvent(payload.data)) { -// ... -``` - -In this particular case, we call `scriptUtils.sendAnonymousChat` that is doing the work of displaying the chat message. - -## Scripting API entry point - -The `WA` object originates from the scripting API. This script is hosted on the front server, at `https://[front_WA_server]/iframe_api.js.`. - -The entry point for this script is the file `front/src/iframe_api.ts`. -All the other files dedicated to the iframe API are located in the `src/Api/iframe` directory. - -## Utility functions to exchange messages - -In the example above, we already saw you can easily send a message from the iframe to WorkAdventure using the -[`sendToWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L11-L13) utility function. - -Of course, messaging can go the other way around and WorkAdventure can also send messages to the iframes. -We use the [`IFrameListener.postMessage`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/IframeListener.ts#L455-L459) function for this. - -Finally, there is a last type of utility function (a quite powerful one). It is quite common to need to call a function -from the iframe in WorkAdventure, and to expect a response. For those use cases, the iframe API comes with a -[`queryWorkadventure`](http://github.com/thecodingmachine/workadventure/blob/ab075ef6f4974766a3e2de12a230ac4df0954b58/front/src/Api/iframe/IframeApiContribution.ts#L30-L49) utility function. - -## Types - -The JSON messages sent over the postMessage API are strictly defined using Typescript types. -Those types are not defined using classical Typescript interfaces. - -Indeed, Typescript interfaces only exist at compilation time but cannot be enforced on runtime. The postMessage API -is an entry point to WorkAdventure, and as with any entry point, data must be checked (otherwise, a hacker could -send specially crafted JSON packages to try to hack WA). - -In WorkAdventure, we use the [generic-type-guard](https://github.com/mscharley/generic-type-guard) package. This package -allows us to create interfaces AND custom type guards in one go. - -Let's go back at our example. Let's have a look at the JSON message sent when we want to send a chat message from the API: - -```typescript -sendToWorkadventure({ - type: "chat", - data: { - message: message, - author: author, - }, -}); -``` - -The "data" part of the message is defined in `front/src/Api/Events/ChatEvent.ts`: - -```typescript -import * as tg from "generic-type-guard"; - -export const isChatEvent = new tg.IsInterface() - .withProperties({ - message: tg.isString, - author: tg.isString, - }) - .get(); -/** - * A message sent from the iFrame to the game to add a message in the chat. - */ -export type ChatEvent = tg.GuardedType; -``` - -Using the generic-type-guard library, we start by writing a type guard function (`isChatEvent`). -From this type guard, the library can automatically generate the `ChatEvent` type that we can refer in our code. - -The advantage of this technique is that, **at runtime**, WorkAdventure can verify that the JSON message received -over the postMessage API is indeed correctly formatted. - -If you are not familiar with Typescript type guards, you can read [an introduction to type guards in the Typescript documentation](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards). - -### Typing one way messages - -For "one-way" messages (from the iframe to WorkAdventure), the `sendToWorkadventure` method expects the passed -object to be of type `IframeEvent`. - -Note: I'd like here to thank @jonnytest1 for helping set up this type system. It rocks ;) - -The `IFrameEvent` type is defined in `front/src/Api/Events/IframeEvent.ts`: - -```typescript -export type IframeEventMap = { - loadPage: LoadPageEvent; - chat: ChatEvent; - openPopup: OpenPopupEvent; - closePopup: ClosePopupEvent; - openTab: OpenTabEvent; - // ... - // All the possible messages go here - // The key goes into the "type" JSON property - // ... -}; -export interface IframeEvent { - type: T; - data: IframeEventMap[T]; -} -``` - -Similarly, if you want to type messages from WorkAdventure to the iframe, there is a very similar `IframeResponseEvent`. - -```typescript -export interface IframeResponseEventMap { - userInputChat: UserInputChatEvent; - enterEvent: EnterLeaveEvent; - leaveEvent: EnterLeaveEvent; - // ... - // All the possible messages go here - // The key goes into the "type" JSON property - // ... -} -export interface IframeResponseEvent { - type: T; - data: IframeResponseEventMap[T]; -} -``` - -### Typing queries (messages with answers) - -If you want to add a new "query" (if you are using the `queryWorkadventure` utility function), you will need to -define the type of the query and the type of the response. - -The signature of `queryWorkadventure` is: - -```typescript -function queryWorkadventure( - content: IframeQuery -): Promise -``` - -Yes, that's a bit cryptic. Hopefully, all you need to know is that to add a new query, you need to edit the `iframeQueryMapTypeGuards` -array in `front/src/Api/Events/IframeEvent.ts`: - -```typescript -export const iframeQueryMapTypeGuards = { - openCoWebsite: { - query: isOpenCoWebsiteEvent, - answer: isCoWebsite, - }, - getCoWebsites: { - query: tg.isUndefined, - answer: tg.isArray(isCoWebsite), - }, - // ... - // the `query` key points to the type guard of the query - // the `answer` key points to the type guard of the response -}; -``` - -### Responding to a query on the WorkAdventure side - -In the WorkAdventure code, each possible query should be handled by what we call an "answerer". - -Registering an answerer happens using the `iframeListener.registerAnswerer()` method. - -Here is a sample: - -```typescript -iframeListener.registerAnswerer("openCoWebsite", (openCoWebsiteEvent, source) => { - // ... - - return /*...*/; -}); -``` - -The `registerAnswerer` callback is passed the event, and should return a response (or a promise to the response) in the expected format -(the one you defined in the `answer` key of `iframeQueryMapTypeGuards`). - -Important: - -- there can be only one answerer registered for a given query type. -- if the answerer is not valid any more, you need to unregister the answerer using `iframeListener.unregisterAnswerer`. - - -## sendToWorkadventure VS queryWorkadventure - -- `sendToWorkadventure` is used to send messages one way from the iframe to WorkAdventure. No response is expected. In particular - if an error happens in WorkAdventure, the iframe will not be notified. -- `queryWorkadventure` is used to send queries that expect an answer. If an error happens in WorkAdventure (i.e. if an - exception is raised), the exception will be propagated to the iframe. - -Because `queryWorkadventure` handles exceptions properly, it can be interesting to use `queryWorkadventure` instead -of `sendToWorkadventure`, even for "one-way" messages. The return message type is simply `undefined` in this case. - diff --git a/docs/dev/how-to-translate.md b/docs/dev/how-to-translate.md deleted file mode 100644 index b72b045a..00000000 --- a/docs/dev/how-to-translate.md +++ /dev/null @@ -1,76 +0,0 @@ -# How to translate WorkAdventure - -We use the [typesafe-i18n](https://github.com/ivanhofer/typesafe-i18n) package to handle the translation. - -## Add a new language - -It is very easy to add a new language! - -First, in the `front/src/i18n` folder create a new folder with the language code as name (the language code according to [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646)). - -In the previously created folder, add a file named index.ts with the following content containing your language information (french from France in this example): - -```ts -import type { Translation } from "../i18n-types"; - -const fr_FR: Translation = { - ...en_US, - language: "Français", - country: "France", -}; - -export default fr_FR; -``` - -## Add a new key - -### Add a simple key - -The keys are searched by a path through the properties of the sub-objects and it is therefore advisable to write your translation as a JavaScript object. - -Please use kamelcase to name your keys! - -Example: - -```ts -{ - messages: { - coffeMachine: { - start: "Coffe machine has been started!"; - } - } -} -``` - -In the code you can translate using `$LL`: - -```ts -import LL from "../../i18n/i18n-svelte"; - -console.log($LL.messages.coffeMachine.start()); -``` - -### Add a key with parameters - -You can also use parameters to make the translation dynamic. -Use the tag { [parameter name] } to apply your parameters in the translations - -Example: - -```ts -{ - messages: { - coffeMachine: { - playerStart: "{ playerName } started the coffee machine!"; - } - } -} -``` - -In the code you can use it like this: - -```ts -$LL.messages.coffeMachine.playerStart.start({ - playerName: "John", -}); -``` diff --git a/docs/dev/images/scripting_1.svg b/docs/dev/images/scripting_1.svg deleted file mode 100644 index cae529f3..00000000 --- a/docs/dev/images/scripting_1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/dev/images/scripting_2.svg b/docs/dev/images/scripting_2.svg deleted file mode 100644 index a07294f4..00000000 --- a/docs/dev/images/scripting_2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/diagrams.md b/docs/diagrams.md deleted file mode 100644 index 9a2f9242..00000000 --- a/docs/diagrams.md +++ /dev/null @@ -1,27 +0,0 @@ -Diagrams are rendered with "mermaid.js" - -sequenceDiagram - participant Br as Browser - participant P as Pusher - participant Ba as Back - participant A as Admin - Note left of Br: The user has a JWT token - Br->>P: /verify: verify the token (no answer) - P->>A: /api/check-user: the user must not be banned - A->>P: 200 (no body) - P->>Br: 200 (no body) - Br->>P: connect to websocket /room?roomid=... - P->>A: /api/check-user(uuid): the user must not be banned - A->>P: 200 (no body) - P->>A: /api/membership(uuid): get data from the user - A->>P: 200 (user data) - Note right of P: Zones are computed on Pusher - P->>Ba: doJoinRoom + list of listened zones - Ba->>P: RoomJoinedMessage - P->>Br: RoomJoinedMessage (users/groups/items/current user id) - P->>A: /api/membership(uuid): get data from the user (again!) - A->>P: 200 (user data) - P->>Br: SendUserMessage (if any message to send) - - - diff --git a/docs/maps/animations.md b/docs/maps/animations.md deleted file mode 100644 index 6ac69e0c..00000000 --- a/docs/maps/animations.md +++ /dev/null @@ -1,33 +0,0 @@ -{.section-title.accent.text-primary} -# Animating WorkAdventure maps - -A tile can run an animation in loops, for example to render water or blinking lights. Each animation frame is a single -32x32 tile. To create an animation, edit the tileset in Tiled and click on the tile to animate (or pick a free tile to -not overwrite existing ones) and click on the animation editor: - - -
- -
- -You can now add all tiles that should be part of the animation via drag and drop to the "playlist" and adjust the frame duration: - -
-
- -
The tile animation editor
-
-
- -You can preview animations directly in Tiled, using the "Show tile animations" option: - - -
-
- -
The Show Tile Animations option
-
-
- -{.alert.alert-info} -**Tip:** The engine does tile-updates every 100ms, animations with a shorter frame duration will most likely not look that good or may even do not work. diff --git a/docs/maps/api-camera.md b/docs/maps/api-camera.md deleted file mode 100644 index f0974ad6..00000000 --- a/docs/maps/api-camera.md +++ /dev/null @@ -1,50 +0,0 @@ -{.section-title.accent.text-primary} - -# API Camera functions Reference - -### Start following player - -```javascript -WA.camera.followPlayer(smooth: boolean): void -``` -Set camera to follow the player. Set `smooth` to true for smooth transition. - -### Set spot for camera to look at - -```javascript -WA.camera.set( - x: number, - y: number, - width?: number, - height?: number, - lock: boolean = false, - smooth: boolean = false, -): void -``` - -Set camera to look at given spot. -Setting `width` and `height` will adjust zoom. -Set `lock` to true to lock camera in this position. -Set `smooth` to true for smooth transition. - -### Listen to camera updates - -``` -WA.camera.onCameraUpdate(): Subscription -``` - -Listens to updates of the camera viewport. It will trigger for every update of the camera's properties (position or scale for instance). An event will be sent. - -The event has the following attributes : -* **x (number):** coordinate X of the camera's world view (the area looked at by the camera). -* **y (number):** coordinate Y of the camera's world view. -* **width (number):** the width of the camera's world view. -* **height (number):** the height of the camera's world view. - -**callback:** the function that will be called when the camera is updated. - -Example : -```javascript -const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView)); -//later... -subscription.unsubscribe(); diff --git a/docs/maps/api-chat.md b/docs/maps/api-chat.md deleted file mode 100644 index 49a40f97..00000000 --- a/docs/maps/api-chat.md +++ /dev/null @@ -1,37 +0,0 @@ -{.section-title.accent.text-primary} -# API Chat functions reference - -### Sending a message in the chat - -``` -WA.chat.sendChatMessage(message: string, author: string): void -``` - -Sends a message in the chat. The message is only visible in the browser of the current user. - -* **message**: the message to be displayed in the chat -* **author**: the name displayed for the author of the message. It does not have to be a real user. - -Example: - -```javascript -WA.chat.sendChatMessage('Hello world', 'Mr Robot'); -``` - -### Listening to messages from the chat - -```javascript -WA.chat.onChatMessage(callback: (message: string) => void): void -``` - -Listens to messages typed by the current user and calls the callback. Messages from other users in the chat cannot be listened to. - -* **callback**: the function that will be called when a message is received. It contains the message typed by the user. - -Example: - -```javascript -WA.chat.onChatMessage((message => { - console.log('The user typed a message', message); -})); -``` diff --git a/docs/maps/api-controls.md b/docs/maps/api-controls.md deleted file mode 100644 index c2b47262..00000000 --- a/docs/maps/api-controls.md +++ /dev/null @@ -1,29 +0,0 @@ -{.section-title.accent.text-primary} -# API Controls functions Reference - -### Disabling / restoring controls - -``` -WA.controls.disablePlayerControls(): void -WA.controls.restorePlayerControls(): void -``` - -These 2 methods can be used to completely disable player controls and to enable them again. - -When controls are disabled, the user cannot move anymore using keyboard input. This can be useful in a "First Time User Experience" part, to display an important message to a user before letting him/her move again. - -Example: - -```javascript -WA.room.onEnterLayer('myZone').subscribe(() => { - WA.controls.disablePlayerControls(); - WA.ui.openPopup("popupRectangle", 'This is an imporant message!', [{ - label: "Got it!", - className: "primary", - callback: (popup) => { - WA.controls.restorePlayerControls(); - popup.close(); - } - }]); -}) -``` diff --git a/docs/maps/api-deprecated.md b/docs/maps/api-deprecated.md deleted file mode 100644 index ffa8af9e..00000000 --- a/docs/maps/api-deprecated.md +++ /dev/null @@ -1,23 +0,0 @@ -{.section-title.accent.text-primary} -# API Reference - Deprecated functions - -The list of functions below is **deprecated**. You should not use those but. use the replacement functions. - -- Method `WA.sendChatMessage` is deprecated. It has been renamed to [`WA.chat.sendChatMessage`](api-chat.md#sending-a-message-in-the-chat). -- Method `WA.disablePlayerControls` is deprecated. It has been renamed to [`WA.controls.disablePlayerControls`](api-controls.md#disabling--restoring-controls). -- Method `WA.restorePlayerControls` is deprecated. It has been renamed to [`WA.controls.restorePlayerControls`](api-controls.md#disabling--restoring-controls). -- Method `WA.displayBubble` is deprecated. It has been renamed to `WA.ui.displayBubble`. -- Method `WA.removeBubble` is deprecated. It has been renamed to `WA.ui.removeBubble`. -- Method `WA.openTab` is deprecated. It has been renamed to [`WA.nav.openTab`](api-nav.md#opening-a-web-page-in-a-new-tab). -- Method `WA.loadSound` is deprecated. It has been renamed to [`WA.sound.loadSound`](api-sound.md#load-a-sound-from-an-url). -- Method `WA.goToPage` is deprecated. It has been renamed to [`WA.nav.goToPage`](api-nav.md#opening-a-web-page-in-the-current-tab). -- Method `WA.goToRoom` is deprecated. It has been renamed to [`WA.nav.goToRoom`](api-nav.md#going-to-a-different-map-from-the-script). -- Method `WA.openCoWebSite` is deprecated. It has been renamed to [`WA.nav.openCoWebSite`](api-nav.md#openingclosing-web-page-in-co-websites). -- Method `WA.closeCoWebSite` is deprecated. It has been remove and [replace by a function close](api-nav.md#openingclosing-web-page-in-co-websites). -- Method `WA.openPopup` is deprecated. It has been renamed to [`WA.ui.openPopup`](api-ui.md#opening-a-popup). -- Method `WA.onChatMessage` is deprecated. It has been renamed to [`WA.chat.onChatMessage`](api-chat.md#listening-to-messages-from-the-chat). -- Method `WA.onEnterZone` is deprecated. It has been renamed to [`WA.room.onEnterZone`](api-room.md#detecting-when-the-user-entersleaves-a-layer). -- Method `WA.onLeaveZone` is deprecated. It has been renamed to [`WA.room.onLeaveZone`](api-room.md#detecting-when-the-user-entersleaves-a-layer). -- Method `WA.ui.registerMenuCommand` parameter `callback` is deprecated. Use [`WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions)`](api-ui.md#add-custom-menu). -- Method `WA.room.onEnterZone` is deprecated. Use instead [`WA.room.onEnterLayer`](api-room.md#detecting-when-the-user-entersleaves-a-layer). -- Method `WA.room.onLeaveZone` is deprecated. Use instead [`WA.room.onLeaveLayer`](api-room.md#detecting-when-the-user-entersleaves-a-layer). \ No newline at end of file diff --git a/docs/maps/api-nav.md b/docs/maps/api-nav.md deleted file mode 100644 index 8d9fe595..00000000 --- a/docs/maps/api-nav.md +++ /dev/null @@ -1,81 +0,0 @@ -{.section-title.accent.text-primary} -# API Navigation functions reference - -### Opening a web page in a new tab - -```ts -WA.nav.openTab(url: string): void -``` - -Opens the webpage at "url" in your browser, in a new tab. - -Example: - -```ts -WA.nav.openTab('https://www.wikipedia.org/'); -``` - -### Opening a web page in the current tab - -```ts -WA.nav.goToPage(url: string): void -``` - -Opens the webpage at "url" in your browser in place of WorkAdventure. WorkAdventure will be completely unloaded. - -Example: - -```ts -WA.nav.goToPage('https://www.wikipedia.org/'); -``` - -### Going to a different map from the script - -```ts -WA.nav.goToRoom(url: string): void -``` - -Load the map at url without unloading workadventure - -relative urls: "../subFolder/map.json[#start-layer-name]" -global urls: "/_/global/domain/path/map.json[#start-layer-name]" - -Example: - -```ts -WA.nav.goToRoom("/@/tcm/workadventure/floor0") // workadventure urls -WA.nav.goToRoom('../otherMap/map.json'); -WA.nav.goToRoom("/_/global/.json#start-layer-2") -``` - -### Opening/closing web page in Co-Websites - -```ts -WA.nav.openCoWebSite(url: string, allowApi?: boolean = false, allowPolicy?: string = "", percentWidth?: number, position?: number, closable?: boolean, lazy?: boolean): Promise -``` - -Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow),widthPercent define the width of the main cowebsite beetween the min size and the max size (70% of the viewport), position in whitch slot the web page will be open, closable allow to close the webpage also you need to close it by the api and lazy -it's to add the cowebsite but don't load it. - -Example: - -```ts -const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/'); -const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 70, 1, true, true); -// ... -coWebsite.close(); -``` - -### Get all Co-Websites - -```ts -WA.nav.getCoWebSites(): Promise -``` - -Get all opened co-websites with their ids and positions. - -Example: - -```ts -const coWebsites = await WA.nav.getCowebSites(); -``` diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md deleted file mode 100644 index c3aa808a..00000000 --- a/docs/maps/api-player.md +++ /dev/null @@ -1,244 +0,0 @@ -{.section-title.accent.text-primary} -# API Player functions Reference - -### Get the player name - -``` -WA.player.name: string; -``` - -The player name is available from the `WA.player.name` property. - -{.alert.alert-info} -You need to wait for the end of the initialization before accessing `WA.player.name` - -```typescript -WA.onInit().then(() => { - console.log('Player name: ', WA.player.name); -}) -``` - -### Get the player ID - -``` -WA.player.id: string|undefined; -``` - -The player ID is available from the `WA.player.id` property. -This is a unique identifier for a given player. Anonymous player might not have an id. - -{.alert.alert-info} -You need to wait for the end of the initialization before accessing `WA.player.id` - -```typescript -WA.onInit().then(() => { - console.log('Player ID: ', WA.player.id); -}) -``` - -### Get the player language - -``` -WA.player.language: string; -``` - -The current language of player is available from the `WA.player.language` property. - -{.alert.alert-info} -You need to wait for the end of the initialization before accessing `WA.player.language` - -```typescript -WA.onInit().then(() => { - console.log('Player language: ', WA.player.language); -}) -``` - -### Get the tags of the player - -``` -WA.player.tags: string[]; -``` - -The player tags are available from the `WA.player.tags` property. -They represent a set of rights the player acquires after login in. - -{.alert.alert-warn} -Tags attributed to a user depend on the authentication system you are using. For the hosted version -of WorkAdventure, you can define tags related to the user in the [administration panel](https://workadventu.re/admin-guide/manage-members). - -{.alert.alert-info} -You need to wait for the end of the initialization before accessing `WA.player.tags` - -```typescript -WA.onInit().then(() => { - console.log('Tags: ', WA.player.tags); -}) -``` - -### Get the position of the player -``` -WA.player.getPosition(): Promise -``` -The player's current position is available using the `WA.player.getPosition()` function. - -`Position` has the following attributes : -* **x (number) :** The coordinate x of the current player's position. -* **y (number) :** The coordinate y of the current player's position. - - -{.alert.alert-info} -You need to wait for the end of the initialization before calling `WA.player.getPosition()` - -```typescript -WA.onInit().then(() => { - console.log('Position: ', WA.player.getPosition()); -}) -``` - - -### Get the user-room token of the player - -``` -WA.player.userRoomToken: string; -``` - -The user-room token is available from the `WA.player.userRoomToken` property. - -This token can be used by third party services to authenticate a player and prove that the player is in a given room. -The token is generated by the administration panel linked to WorkAdventure. The token is a string and is depending on your implementation of the administration panel. -In WorkAdventure SAAS version, the token is a JWT token that contains information such as the player's room ID and its associated membership ID. - -If you are using the self-hosted version of WorkAdventure and you developed your own administration panel, the token can be anything. -By default, self-hosted versions of WorkAdventure don't come with an administration panel, so the token string will be empty. - -{.alert.alert-info} -A typical use-case for the user-room token is providing logo upload capabilities in a map. -The token can be used as a way to authenticate a WorkAdventure player and ensure he is indeed in the map and authorized to upload a logo. - -{.alert.alert-info} -You need to wait for the end of the initialization before accessing `WA.player.userRoomToken` - -```typescript -WA.onInit().then(() => { - console.log('Token: ', WA.player.userRoomToken); -}) -``` - -### Get the position of the player -``` -WA.player.getPosition(): Promise -``` -The player's current position is available using the `WA.player.getPosition()` function. - -`Position` has the following attributes : -* **x (number) :** The coordinate x of the current player's position. -* **y (number) :** The coordinate y of the current player's position. - - -{.alert.alert-info} -You need to wait for the end of the initialization before calling `WA.player.getPosition()` - -```typescript -WA.onInit().then(async () => { - console.log('Position: ', await WA.player.getPosition()); -}) -``` - - -### Listen to player movement -``` -WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void; -``` -Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction. - -The event has the following attributes : -* **moving (boolean):** **true** when the current player is moving, **false** otherwise. -* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving. -* **x (number):** coordinate X of the current player. -* **y (number):** coordinate Y of the current player. -* **oldX (number):** old coordinate X of the current player. -* **oldY (number):** old coordinate Y of the current player. - -**callback:** the function that will be called when the current player is moving. It contains the event. - -Example : -```javascript -WA.player.onPlayerMove(console.log); -``` - -## Player specific variables -Similarly to maps (see [API state related functions](api-state.md)), it is possible to store data **related to a specific player** in a "state". Such data will be stored using the local storage from the user's browser. Any value that is serializable in JSON can be stored. - -{.alert.alert-info} -In the future, player-related variables will be stored on the WorkAdventure server if the current player is logged. - -Any value that is serializable in JSON can be stored. - -### Setting a property -A player property can be set simply by assigning a value. - -Example: -```javascript -WA.player.state.toto = "value" //will set the "toto" key to "value" -``` - -### Reading a variable -A player variable can be read by calling its key from the player's state. - -Example: -```javascript -WA.player.state.toto //will retrieve the variable -``` - -### Move player to position -```typescript -WA.player.moveTo(x: number, y: number, speed?: number): Promise<{ x: number, y: number, cancelled: boolean }>; -``` -Player will try to find shortest path to the destination point and proceed to move there. -```typescript -// Let's move player to x: 250 y: 250 with speed of 10 -WA.player.moveTo(250, 250, 10); -``` -You can also chain movement like this: -```typescript -// Player will move to the next point after reaching first one -await WA.player.moveTo(250, 250, 10); -await WA.player.moveTo(500, 0, 10); -``` -Or like this: -```typescript -// Player will move to the next point after reaching first one or stop if the movement was cancelled -WA.player.moveTo(250, 250, 10).then((result) => { - if (!result.cancelled) { - WA.player.moveTo(500, 0, 10); - } -}); -``` -It is possible to get the information about current player's position on stop and if the movement was interrupted -```typescript -// Result will store x and y of Player at the moment of movement's end and information if the movement was interrupted -const result = await WA.player.moveTo(250, 250, 10); -// result: { x: number, y: number, cancelled: boolean } -``` - -### Set the outline color of the player -``` -WA.player.setOutlineColor(red: number, green: number, blue: number): Promise; -WA.player.removeOutlineColor(): Promise; -``` - -You can display a thin line around your player's name (the "outline"). - -Use `setOutlineColor` to set the outline and `removeOutlineColor` to remove it. - -Colors are expressed in RGB. Each parameter is an integer between 0 and 255. - -```typescript -// Let's add a red outline to our player -WA.player.setOutlineColor(255, 0, 0); -``` - -When you set the outline on your player, other players will see the outline too (the outline color is shared across -browsers automatically). - -![](images/outlines.png) diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md deleted file mode 100644 index a0869075..00000000 --- a/docs/maps/api-reference.md +++ /dev/null @@ -1,15 +0,0 @@ -{.section-title.accent.text-primary} -# API Reference - -- [Start / Init functions](api-start.md) -- [Navigation functions](api-nav.md) -- [Chat functions](api-chat.md) -- [Room functions](api-room.md) -- [State related functions](api-state.md) -- [Player functions](api-player.md) -- [UI functions](api-ui.md) -- [Sound functions](api-sound.md) -- [Controls functions](api-controls.md) -- [Camera functions](api-camera.md) - -- [List of deprecated functions](api-deprecated.md) diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md deleted file mode 100644 index fa315abf..00000000 --- a/docs/maps/api-room.md +++ /dev/null @@ -1,287 +0,0 @@ -{.section-title.accent.text-primary} - -# API Room functions Reference - -### Working with group layers - -If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names -together. - -Example : - -
-
- -
-
- -The name of the layers of this map are : - -- `entries/start` -- `bottom/ground/under` -- `bottom/build/carpet` -- `wall` - -### Detecting when the user enters/leaves a layer - -``` -WA.room.onEnterLayer(name: string): Subscription -WA.room.onLeaveLayer(name: string): Subscription -``` - -Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer. - -- **name**: the name of the layer who as defined in Tiled. - -Example: - -```ts -const myLayerSubscriber = WA.room.onEnterLayer("myLayer").subscribe(() => { - WA.chat.sendChatMessage("Hello!", "Mr Robot"); -}); - -WA.room.onLeaveLayer("myLayer").subscribe(() => { - WA.chat.sendChatMessage("Goodbye!", "Mr Robot"); - myLayerSubscriber.unsubscribe(); -}); -``` - -### Show / Hide a layer - -``` -WA.room.showLayer(layerName : string): void -WA.room.hideLayer(layerName : string) : void -``` - -These 2 methods can be used to show and hide a layer. if `layerName` is the name of a group layer, show/hide all the -layer in that group layer. - -Example : - -```ts -WA.room.showLayer("bottom"); -//... -WA.room.hideLayer("bottom"); -``` - -### Set/Create properties in a layer - -``` -WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void; -``` - -Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, -create the property `propertyName` and set the value of the property at `propertyValue`. - -Note : -To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`. - -Example : - -```ts -WA.room.setProperty("wikiLayer", "openWebsite", "https://www.wikipedia.org/"); -``` - -### Get the room id - -``` -WA.room.id: string; -``` - -The ID of the current room is available from the `WA.room.id` property. - -{.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.id` - -```typescript -WA.onInit().then(() => { - console.log("Room id: ", WA.room.id); - // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json" -}); -``` - -### Get the map URL - -``` -WA.room.mapURL: string; -``` - -The URL of the map is available from the `WA.room.mapURL` property. - -{.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.mapURL` - -```typescript -WA.onInit().then(() => { - console.log("Map URL: ", WA.room.mapURL); - // Will output something like: 'https://mymap.org/map.json" -}); -``` - -### Getting map data - -``` -WA.room.getTiledMap(): Promise -``` - -Returns a promise that resolves to the JSON map file. - -```ts -const map = await WA.room.getTiledMap(); -console.log("Map generated with Tiled version ", map.tiledversion); -``` - -Check -the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/) -. - -### Changing tiles - -``` -WA.room.setTiles(tiles: TileDescriptor[]): void -``` - -Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`. - -If `tile` is a string, it's not the id of the tile but the value of the property `name`. - -
-
- -
-
- -`TileDescriptor` has the following attributes : - -- **x (number) :** The coordinate x of the tile that you want to replace. -- **y (number) :** The coordinate y of the tile that you want to replace. -- **tile (number | string) :** The id of the tile that will be placed in the map. -- **layer (string) :** The name of the layer where the tile will be placed. - -**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want -to the id of the tile in Tiled Editor. - -Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`. - -Example : - -```ts -WA.room.setTiles([ - { x: 6, y: 4, tile: "blue", layer: "setTiles" }, - { x: 7, y: 4, tile: 109, layer: "setTiles" }, - { x: 8, y: 4, tile: 109, layer: "setTiles" }, - { x: 9, y: 4, tile: "blue", layer: "setTiles" }, -]); -``` - -### Loading a tileset - -``` -WA.room.loadTileset(url: string): Promise -``` - -Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset. - -You can create a tileset file in Tile Editor. - -```ts -WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { - WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: "bottom" }]); -}); -``` - -## Embedding websites in a map - -You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using -the ["website" objects](website-in-map.md)). - -### Getting an instance of a website already embedded in the map - -``` -WA.room.website.get(objectName: string): Promise -``` - -You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of -an `EmbeddedWebsite` instance. - -```ts -// Get an existing website object where 'my_website' is the name of the object (on any layer object of the map) -const website = await WA.room.website.get("my_website"); -website.url = "https://example.com"; -website.visible = true; -``` - -### Adding a new website in a map - -``` -WA.room.website.create(website: CreateEmbeddedWebsiteEvent): EmbeddedWebsite - -interface CreateEmbeddedWebsiteEvent { - name: string; // A unique name for this iframe - url: string; // The URL the iframe points to. - position: { - x: number, // In "game" pixels, relative to the map or player coordinates, depending on origin - y: number, // In "game" pixels, relative to the map or player coordinates, depending on origin - width: number, // In "game" pixels - height: number, // In "game" pixels - }, - visible?: boolean, // Whether to display the iframe or not - allowApi?: boolean, // Whether the scripting API should be available to the iframe - allow?: string, // The list of feature policies allowed - origin: "player" | "map" // The origin used to place the x and y coordinates of the iframe's top-left corner, defaults to "map" - scale: number, // A ratio used to resize the iframe -} -``` - -You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns -an `EmbeddedWebsite` instance. - -```ts -// Create a new website object -const website = WA.room.website.create({ - name: "my_website", - url: "https://example.com", - position: { - x: 64, - y: 128, - width: 320, - height: 240, - }, - visible: true, - allowApi: true, - allow: "fullscreen", - origin: "map", - scale: 1, -}); -``` - -### Deleting a website from a map - -``` -WA.room.website.delete(name: string): Promise -``` - -Use `WA.room.website.delete` to completely remove an embedded website from your map. - -### The EmbeddedWebsite class - -Instances of the `EmbeddedWebsite` class represent the website displayed on the map. - -```typescript -class EmbeddedWebsite { - readonly name: string; - url: string; - visible: boolean; - allow: string; - allowApi: boolean; - x: number; // In "game" pixels, relative to the map or player coordinates, depending on origin - y: number; // In "game" pixels, relative to the map or player coordinates, depending on origin - width: number; // In "game" pixels - height: number; // In "game" pixels - origin: "player" | "map"; - scale: number; -} -``` - -When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map. - -{.alert.alert-warning} The websites you add/edit/delete via the scripting API are only shown locally. If you want them -to be displayed for every player, you can use [variables](api-start.md) to share a common state between all users. diff --git a/docs/maps/api-sound.md b/docs/maps/api-sound.md deleted file mode 100644 index 3c57ecae..00000000 --- a/docs/maps/api-sound.md +++ /dev/null @@ -1,34 +0,0 @@ -{.section-title.accent.text-primary} -# API Sound functions Reference - -### Load a sound from an url - -``` -WA.sound.loadSound(url: string): Sound -``` - -Load a sound from an url - -Please note that `loadSound` returns an object of the `Sound` class - -The `Sound` class that represents a loaded sound contains two methods: `play(soundConfig : SoundConfig|undefined)` and `stop()` - -The parameter soundConfig is optional, if you call play without a Sound config the sound will be played with the basic configuration. - -Example: - -```javascript -var mySound = WA.sound.loadSound("Sound.ogg"); -var config = { - volume : 0.5, - loop : false, - rate : 1, - detune : 1, - delay : 0, - seek : 0, - mute : false -} -mySound.play(config); -// ... -mySound.stop(); -``` diff --git a/docs/maps/api-start.md b/docs/maps/api-start.md deleted file mode 100644 index 0fcfc62d..00000000 --- a/docs/maps/api-start.md +++ /dev/null @@ -1,30 +0,0 @@ -{.section-title.accent.text-primary} -# API start functions Reference - -### Waiting for WorkAdventure API to be available - -When your script / iFrame loads WorkAdventure, it takes a few milliseconds for your script / iFrame to exchange -data with WorkAdventure. You should wait for the WorkAdventure API to be fully ready using the `WA.onInit()` method. - -``` -WA.onInit(): Promise -``` - -Some properties (like the current user name, or the room ID) are not available until `WA.onInit` has completed. - -Example: - -```typescript -WA.onInit().then(() => { - console.log('Current player name: ', WA.player.name); -}); -``` - -Or the same code, using await/async: - -```typescript -(async () => { - await WA.onInit(); - console.log('Current player name: ', WA.player.name); -})(); -``` diff --git a/docs/maps/api-state.md b/docs/maps/api-state.md deleted file mode 100644 index a8ee5589..00000000 --- a/docs/maps/api-state.md +++ /dev/null @@ -1,104 +0,0 @@ -{.section-title.accent.text-primary} -# API state related functions Reference - -## Saving / loading state - -The `WA.state` functions allow you to easily share a common state between all the players in a given room. -Moreover, `WA.state` functions can be used to persist this state across reloads. - -``` -WA.state.saveVariable(key : string, data : unknown): void -WA.state.loadVariable(key : string) : unknown -WA.state.hasVariable(key : string) : boolean -WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription -WA.state.[any property]: unknown -``` - -These methods and properties can be used to save, load and track changes in [variables related to the current room](variables.md). - -Variables stored in `WA.state` can be any value that is serializable in JSON. - -Please refrain from storing large amounts of data in a room. Those functions are typically useful for saving or restoring -configuration / metadata. - -{.alert.alert-warning} -We are in the process of fine-tuning variables, and we will eventually put limits on the maximum size a variable can hold. We will also put limits on the number of calls you can make to saving variables, so don't change the value of a variable every 10ms, this will fail in the future. - - -Example : -```javascript -WA.state.saveVariable('config', { - 'bottomExitUrl': '/@/org/world/castle', - 'topExitUrl': '/@/org/world/tower', - 'enableBirdSound': true -}).catch(e => console.error('Something went wrong while saving variable', e)); -//... -let config = WA.state.loadVariable('config'); -``` - -You can use the shortcut properties to load and save variables. The code above is similar to: - -```javascript -WA.state.config = { - 'bottomExitUrl': '/@/org/world/castle', - 'topExitUrl': '/@/org/world/tower', - 'enableBirdSound': true -}; - -//... -let config = WA.state.config; -``` - -Note: `saveVariable` returns a promise that will fail in case the variable cannot be saved. This -can happen if your user does not have the required rights (more on that in the next chapter). -In contrast, if you use the WA.state properties, you cannot access the promise and therefore cannot -know for sure if your variable was properly saved. - -If you are using Typescript, please note that the type of variables is `unknown`. This is -for security purpose, as we don't know the type of the variable. In order to use the returned value, -you will need to cast it to the correct type (or better, use a [Type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to actually check at runtime -that you get the expected type). - -{.alert.alert-warning} -For security reasons, the list of variables you are allowed to access and modify is **restricted** (otherwise, anyone on your map could set any data). -Variables storage is subject to an authorization process. Read below to learn more. - -## Defining a variable - -Out of the box, you cannot edit *any* variable. Variables MUST be declared in the map. - -Check the [dedicated variables page](variables.md) to learn how to declare a variable in a map. - -## Tracking variables changes - -The properties of the `WA.state` object are shared in real-time between users of a same room. You can listen to modifications -of any property of `WA.state` by using the `WA.state.onVariableChange()` method. - -``` -WA.state.onVariableChange(name: string): Observable -``` - -Usage: - -```javascript -WA.state.onVariableChange('config').subscribe((value) => { - console.log('Variable "config" changed. New value: ', value); -}); -``` - -The `WA.state.onVariableChange` method returns an [RxJS `Observable` object](https://rxjs.dev/guide/observable). This is -an object on which you can add subscriptions using the `subscribe` method. - -### Stopping tracking variables - -If you want to stop tracking a variable change, the `subscribe` method returns a subscription object with an `unsubscribe` method. - -**Example with unsubscription:** - -```javascript -const subscription = WA.state.onVariableChange('config').subscribe((value) => { - console.log('Variable "config" changed. New value: ', value); -}); -// Later: -subscription.unsubscribe(); -``` diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md deleted file mode 100644 index 8583c061..00000000 --- a/docs/maps/api-ui.md +++ /dev/null @@ -1,164 +0,0 @@ -{.section-title.accent.text-primary} -# API UI functions Reference - -### Opening a popup - -In order to open a popup window, you must first define the position of the popup on your map. - -You can position this popup by using a "rectangle" object in Tiled that you will place on an "object" layer. - -
-
- -
-
- -
-
- -``` -WA.ui.openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup -``` - -* **targetObject**: the name of the rectangle object defined in Tiled. -* **message**: the message to display in the popup. -* **buttons**: an array of action buttons defined underneath the popup. - -Action buttons are `ButtonDescriptor` objects containing these properties. - -* **label (_string_)**: The label of the button. -* **className (_string_)**: The visual type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled". -* **callback (_(popup: Popup)=>void_)**: Callback called when the button is pressed. - -Please note that `openPopup` returns an object of the `Popup` class. Also, the callback called when a button is clicked is passed a `Popup` object. - -The `Popup` class that represents an open popup contains a single method: `close()`. This will obviously close the popup when called. - -```javascript -class Popup { - /** - * Closes the popup - */ - close() {}; -} -``` - -Example: - -```javascript -let helloWorldPopup; - -// Open the popup when we enter a given zone -helloWorldPopup = WA.room.onEnterLayer("myZone").subscribe(() => { - WA.ui.openPopup("popupRectangle", 'Hello world!', [{ - label: "Close", - className: "primary", - callback: (popup) => { - // Close the popup when the "Close" button is pressed. - popup.close(); - } - }]); -}); - -// Close the popup when we leave the zone. -WA.room.onLeaveLayer("myZone").subscribe(() => { - helloWorldPopup.close(); -}) -``` - -### Add custom menu - -``` -WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu -``` -Add a custom menu item containing the text `commandDescriptor` in the navbar of the menu. -`options` attribute accepts an object with three properties : -- `callback : (commandDescriptor: string) => void` : A click on the custom menu will trigger the `callback`. -- `iframe: string` : A click on the custom menu will open the `iframe` inside the menu. -- `allowApi?: boolean` : Allow the iframe of the custom menu to use the Scripting API. - -Important : `options` accepts only `callback` or `iframe` not both. - -Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script. - -
-
- -
-
- -
-
- -Example: -```javascript -const menu = WA.ui.registerMenuCommand('menu test', - { - callback: () => { - WA.chat.sendChatMessage('test'); - } - }) - -// Some time later, if you want to remove the menu: -menu.remove(); -``` - -Please note that `registerMenuCommand` returns an object of the `Menu` class. - -The `Menu` class contains a single method: `remove(): void`. This will obviously remove the menu when called. - -```javascript -class Menu { - /** - * Remove the menu - */ - remove() {}; -} -``` - - - -### Awaiting User Confirmation (with space bar) - -``` -WA.ui.displayActionMessage({ - message: string, - callback: () => void, - type?: "message"|"warning", -}): ActionMessage -``` - -Displays a message at the bottom of the screen (that will disappear when space bar is pressed). - -
- -
- -Example: - -```javascript -const triggerMessage = WA.ui.displayActionMessage({ - message: "press 'space' to confirm", - callback: () => { - WA.chat.sendChatMessage("confirmed", "trigger message logic") - } -}); - -setTimeout(() => { - // later - triggerMessage.remove(); -}, 1000) -``` - -Please note that `displayActionMessage` returns an object of the `ActionMessage` class. - -The `ActionMessage` class contains a single method: `remove(): Promise`. This will obviously remove the message when called. - -```javascript -class ActionMessage { - /** - * Hides the message - */ - remove() {}; -} -``` diff --git a/docs/maps/camera.md b/docs/maps/camera.md deleted file mode 100644 index 9e58fcad..00000000 --- a/docs/maps/camera.md +++ /dev/null @@ -1,92 +0,0 @@ -{.section-title.accent.text-primary} -# Working with camera - -## Focusable Zones - -It is possible to define special regions on the map that can make the camera zoom and center on themselves. We call them "Focusable Zones". When player gets inside, his camera view will be altered - focused, zoomed and locked on defined zone, like this: - -
- -
- -### Adding new **Focusable Zone**: - -1. Make sure you are editing an **Object Layer** - -
- -
- -2. Select **Insert Rectangle** tool - -
- -
- -3. Define new object wherever you want. For example, you can make your chilling room event cosier! - -
- -
- -4. Make sure your object is of type "zone"! - -
- -
- -5. Edit this new object and click on **Add Property**, like this: - -
- -
- -6. Add a **bool** property of name *focusable*: - -
- -
- -7. Make sure it's checked! :) - -
- -
- -All should be set up now and your new **Focusable Zone** should be working fine! - -### Defining custom zoom margin: - -If you want, you can add an additional property to control how much should the camera zoom onto focusable zone. - -1. Like before, click on **Add Property** - -
- -
- -2. Add a **float** property of name *zoom_margin*: - -
- -
- -2. Define how much (in percentage value) should the zoom be decreased: - -
- -
- - For example, if you define your zone as a 300x200 rectangle, setting this property to 0.5 *(50%)* means the camera will try to fit within the viewport the entire zone + margin of 50% of its dimensions, so 450x300. - - - No margin defined - -
- -
- - - Margin set to **0.35** - -
- -
\ No newline at end of file diff --git a/docs/maps/entry-exit.md b/docs/maps/entry-exit.md deleted file mode 100644 index 25e530de..00000000 --- a/docs/maps/entry-exit.md +++ /dev/null @@ -1,92 +0,0 @@ -{.section-title.accent.text-primary} -# Entries and exits - -[Building your map - Defined entries and exits](https://www.youtube.com/watch?v=MuhVgu8H7U0) - -## Defining a default entry point - -In order to define a default start position, you MUST create a layer named "`start`" on your map. This layer MUST contain at least one tile. The players will start on the tile of this layer. If the layer contains many tiles, the players will start randomly on one of those tiles. - -![Start layer screenshot](images/start_layer.png) - -In the screenshot above, the start layer is made of the 2 white tiles. These tiles are not visible to the end user because they are hidden below the "bottom" layer that displays the floor of the map. - -{.alert.alert-info} -**Pro tip**: if you expect many people to connect to your map at the same time (for instance, if you are organizing a big event), consider making a large start zone. This way, users will not all appear at the same position and will not pop randomly in a chat with someone connecting at the same moment. - -## Defining exits - -In order to place an exit on your scene that leads to another scene: - -* You must create a specific layer. When a character reaches ANY tile of that layer, it will exit the scene. -* In layer properties, you MUST add "`exitUrl`" property. It represents the URL of the next scene. You can put relative or absolute URLs. -* If you want to have multiple exits, you can create many layers. Each layer has a different key `exitUrl` and has tiles that represent exits to another scene. - -![](images/exit_layer_map.png) - -{.alert.alert-warning} -**Note:** in older releases of WorkAdventure, you could link to a map file directly using properties `exitSceneUrl` and `exitInstance`. Those properties are now **deprecated**. Use "`exitUrl`" instead. - -## Understanding map URLs in WorkAdventure - -There are 2 kinds of URLs in WorkAdventure: - -* Public URLs are in the form `https://play.workadventu.re/_/[instance]/[server]/[path to map]` -* Private URLs (used in paid accounts) are in the form `https://play.workadventu.re/@/[organization]/[world]/[map]` - -Assuming your JSON map is hosted at "`https://example.com/my/map.json`", then you can browse your map at "`https://play.workadventu.re/_/global/example.com/my/map.json`". Here, "global" is a name of an "instance" of your map. You can put anything instead of "global" here. People on the same instance of the map can see each others. If 2 users use 2 different instances, they are on the same map, but in 2 parallel universes. They cannot see each other. - -## Defining several entry points - -Often your map will have several exits, and therefore, several entry points. For instance, if there is an exit by a door that leads to the garden map, when you come back from the garden you expect to come back by the same door. Therefore, a map can have several entry points. Those entry points are "named" (they have a name). - -In order to create a named entry point: - -You can create a new layer for your entry point or use an existing layer with named tiles. - -* If you don't use the layer named "`start`", you MUST add a boolean "`startLayer`" property to the layer properties. It MUST be set to true. -* If you use this method, when a character enters the map by this entry point, it will enter randomly on ANY tile of that layer. The name of the entry point is the name of that layer. - -![](images/layer-entry-point.png) - -You can also use the tiles properties to create entry point. - -* To do that, you will need to have a layer named "`start`" or with the "`startLayer`" property. Then you MUST add a string "`start`" property to a tile than you use in that layer. The name of the entry point is the value that property. -* If you use this method, when a character enters the map by this entry point, it will enter on ANY tile of the same kind in that layer. - -![](images/tile-entry-point.png) - -Notes : - -* Two tiles with a string "start" property with different value can be in the same layer of entries. -* A tile with a string "start" property that is not in a layer of entries won't usable as an entry point. - -How to use entry point : - -* To enter via this entry point, simply add a hash with the entry point name to the URL ("#[_entryPointName_]"). For instance: "`https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point`". -* You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL) - -## Defining destination point with moveTo parameter - -We are able to direct a Woka to the desired place immediately after spawn. To make users spawn on an entry point and then, walk automatically to a meeting room, simply add `moveTo` as an additional parameter of URL: - -*Use default entry point* -``` -.../my_map.json#&moveTo=exit -``` -*Define entry point and moveTo parameter like this...* -``` -.../my_map.json#start&moveTo=meeting-room -``` -*...or like this* -``` -.../my_map.json#moveTo=meeting-room&start -``` -*...or even like this!* -``` -.../my_map.json#start&moveTo=200,100 -``` - -For this to work, moveTo must be equal to the x and y position, layer name, or object name of interest. Layer should have at least one tile defined. In case of layer having many tiles, user will go to one of them, randomly selected. - -![](images/moveTo-layer-example.png) \ No newline at end of file diff --git a/docs/maps/hosting.md b/docs/maps/hosting.md deleted file mode 100644 index b0d8bc22..00000000 --- a/docs/maps/hosting.md +++ /dev/null @@ -1,30 +0,0 @@ -{.section-title.accent.text-primary} -# Hosting your map - -The [Getting Started](.) page proposes to use a "starter kit" that is relying on GitHub pages for hosting the map. This is a fairly good solution as GitHub pages offers a free and performant hosting. - -But using GitHub pages is not necessary. You can host your maps on any webserver. - -{.alert.alert-warning} -If you decide to host your maps on your own webserver, you must **configure CORS headers** in your browser to allow access from WorkAdventure. - -## Configuring CORS headers - -CORS headers ([Cross Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)) are useful when a website want to make some resources accessible to another website. This is exactly what we want to do. We want the map you are designing to be accessible from the WorkAdventure domain (`play.workadventu.re`). - -{.alert.alert-warning} -If you are using the "scripting API", only allowing the `play.workadventu.re` will not be enough. You will need to allow `*` -as a domain in order to be able to load scripts. If for some reason, you cannot or do not want to allow `*` as a domain, please -read the [scripting internals](scripting-internals.md) guide for alternatives. - -### Enabling CORS for Apache - -In order to enable CORS in your Apache configuration, you will need to ensure the `headers` module is enabled. - -In your Apache configuration file, simply add the following line inside either the ``, ``, `` or `` sections, or within a `.htaccess` file. - - Header set Access-Control-Allow-Origin "*" - -### Enabling CORS on another webserver - -Check out [enable-cors.org](https://enable-cors.org/server.html) which has detailed instructions on how to enable CORS on many different webservers. diff --git a/docs/maps/images/anims/animation_editor.png b/docs/maps/images/anims/animation_editor.png deleted file mode 100644 index 92d00aa7..00000000 Binary files a/docs/maps/images/anims/animation_editor.png and /dev/null differ diff --git a/docs/maps/images/anims/camera.png b/docs/maps/images/anims/camera.png deleted file mode 100644 index c96d0b52..00000000 Binary files a/docs/maps/images/anims/camera.png and /dev/null differ diff --git a/docs/maps/images/anims/settings_show_animations.png b/docs/maps/images/anims/settings_show_animations.png deleted file mode 100644 index bf45e116..00000000 Binary files a/docs/maps/images/anims/settings_show_animations.png and /dev/null differ diff --git a/docs/maps/images/camera/0_focusable_zone.png b/docs/maps/images/camera/0_focusable_zone.png deleted file mode 100644 index 8b54f11f..00000000 Binary files a/docs/maps/images/camera/0_focusable_zone.png and /dev/null differ diff --git a/docs/maps/images/camera/1_object_layer.png b/docs/maps/images/camera/1_object_layer.png deleted file mode 100644 index 6f57d0ae..00000000 Binary files a/docs/maps/images/camera/1_object_layer.png and /dev/null differ diff --git a/docs/maps/images/camera/2_rectangle_zone.png b/docs/maps/images/camera/2_rectangle_zone.png deleted file mode 100644 index 9b0b9cda..00000000 Binary files a/docs/maps/images/camera/2_rectangle_zone.png and /dev/null differ diff --git a/docs/maps/images/camera/3_define_new_zone.png b/docs/maps/images/camera/3_define_new_zone.png deleted file mode 100644 index 226028eb..00000000 Binary files a/docs/maps/images/camera/3_define_new_zone.png and /dev/null differ diff --git a/docs/maps/images/camera/4_add_zone_type.png b/docs/maps/images/camera/4_add_zone_type.png deleted file mode 100644 index 0416d1e4..00000000 Binary files a/docs/maps/images/camera/4_add_zone_type.png and /dev/null differ diff --git a/docs/maps/images/camera/5_click_add_property.png b/docs/maps/images/camera/5_click_add_property.png deleted file mode 100644 index 9aa96a2f..00000000 Binary files a/docs/maps/images/camera/5_click_add_property.png and /dev/null differ diff --git a/docs/maps/images/camera/6_add_focusable_prop.png b/docs/maps/images/camera/6_add_focusable_prop.png deleted file mode 100644 index 3ba1b955..00000000 Binary files a/docs/maps/images/camera/6_add_focusable_prop.png and /dev/null differ diff --git a/docs/maps/images/camera/7_make_sure_checked.png b/docs/maps/images/camera/7_make_sure_checked.png deleted file mode 100644 index 7fbcdb89..00000000 Binary files a/docs/maps/images/camera/7_make_sure_checked.png and /dev/null differ diff --git a/docs/maps/images/camera/8_add_zoom_margin.png b/docs/maps/images/camera/8_add_zoom_margin.png deleted file mode 100644 index 8e3f5256..00000000 Binary files a/docs/maps/images/camera/8_add_zoom_margin.png and /dev/null differ diff --git a/docs/maps/images/camera/9_optional_zoom_margin_defined.png b/docs/maps/images/camera/9_optional_zoom_margin_defined.png deleted file mode 100644 index 8b41d7d0..00000000 Binary files a/docs/maps/images/camera/9_optional_zoom_margin_defined.png and /dev/null differ diff --git a/docs/maps/images/camera/no_margin.png b/docs/maps/images/camera/no_margin.png deleted file mode 100644 index b8c9dd18..00000000 Binary files a/docs/maps/images/camera/no_margin.png and /dev/null differ diff --git a/docs/maps/images/camera/with_margin.png b/docs/maps/images/camera/with_margin.png deleted file mode 100644 index ffd057ea..00000000 Binary files a/docs/maps/images/camera/with_margin.png and /dev/null differ diff --git a/docs/maps/images/click_space_jitsi.png b/docs/maps/images/click_space_jitsi.png deleted file mode 100644 index ec7d7198..00000000 Binary files a/docs/maps/images/click_space_jitsi.png and /dev/null differ diff --git a/docs/maps/images/click_space_open_website.png b/docs/maps/images/click_space_open_website.png deleted file mode 100644 index 09d0daa2..00000000 Binary files a/docs/maps/images/click_space_open_website.png and /dev/null differ diff --git a/docs/maps/images/collides-1.png b/docs/maps/images/collides-1.png deleted file mode 100644 index 54582d05..00000000 Binary files a/docs/maps/images/collides-1.png and /dev/null differ diff --git a/docs/maps/images/collides-2.png b/docs/maps/images/collides-2.png deleted file mode 100644 index a34b18dc..00000000 Binary files a/docs/maps/images/collides-2.png and /dev/null differ diff --git a/docs/maps/images/collides-3.png b/docs/maps/images/collides-3.png deleted file mode 100644 index e775e396..00000000 Binary files a/docs/maps/images/collides-3.png and /dev/null differ diff --git a/docs/maps/images/collides-4.png b/docs/maps/images/collides-4.png deleted file mode 100644 index 024e1b86..00000000 Binary files a/docs/maps/images/collides-4.png and /dev/null differ diff --git a/docs/maps/images/create_repo.png b/docs/maps/images/create_repo.png deleted file mode 100644 index 38083edd..00000000 Binary files a/docs/maps/images/create_repo.png and /dev/null differ diff --git a/docs/maps/images/custom-menu-iframe.png b/docs/maps/images/custom-menu-iframe.png deleted file mode 100644 index 9df2aa5f..00000000 Binary files a/docs/maps/images/custom-menu-iframe.png and /dev/null differ diff --git a/docs/maps/images/custom-menu-navbar.png b/docs/maps/images/custom-menu-navbar.png deleted file mode 100644 index c2440956..00000000 Binary files a/docs/maps/images/custom-menu-navbar.png and /dev/null differ diff --git a/docs/maps/images/groupLayer.png b/docs/maps/images/groupLayer.png deleted file mode 100644 index 83e3cdd5..00000000 Binary files a/docs/maps/images/groupLayer.png and /dev/null differ diff --git a/docs/maps/images/icon_open_website.png b/docs/maps/images/icon_open_website.png deleted file mode 100644 index f3a505bd..00000000 Binary files a/docs/maps/images/icon_open_website.png and /dev/null differ diff --git a/docs/maps/images/layer-entry-point.png b/docs/maps/images/layer-entry-point.png deleted file mode 100644 index 52345800..00000000 Binary files a/docs/maps/images/layer-entry-point.png and /dev/null differ diff --git a/docs/maps/images/mapProperties.png b/docs/maps/images/mapProperties.png deleted file mode 100644 index d4001da4..00000000 Binary files a/docs/maps/images/mapProperties.png and /dev/null differ diff --git a/docs/maps/images/menu-command.png b/docs/maps/images/menu-command.png deleted file mode 100644 index 0caf75c9..00000000 Binary files a/docs/maps/images/menu-command.png and /dev/null differ diff --git a/docs/maps/images/moveTo-layer-example.png b/docs/maps/images/moveTo-layer-example.png deleted file mode 100644 index 12e8a4ad..00000000 Binary files a/docs/maps/images/moveTo-layer-example.png and /dev/null differ diff --git a/docs/maps/images/nameIndexProperty.png b/docs/maps/images/nameIndexProperty.png deleted file mode 100644 index f434c659..00000000 Binary files a/docs/maps/images/nameIndexProperty.png and /dev/null differ diff --git a/docs/maps/images/object_variable.png b/docs/maps/images/object_variable.png deleted file mode 100644 index 434f6191..00000000 Binary files a/docs/maps/images/object_variable.png and /dev/null differ diff --git a/docs/maps/images/open_website.png b/docs/maps/images/open_website.png deleted file mode 100644 index 75023a4e..00000000 Binary files a/docs/maps/images/open_website.png and /dev/null differ diff --git a/docs/maps/images/open_website_allow_api.png b/docs/maps/images/open_website_allow_api.png deleted file mode 100644 index 1289611b..00000000 Binary files a/docs/maps/images/open_website_allow_api.png and /dev/null differ diff --git a/docs/maps/images/open_website_policy.png b/docs/maps/images/open_website_policy.png deleted file mode 100644 index d72e3472..00000000 Binary files a/docs/maps/images/open_website_policy.png and /dev/null differ diff --git a/docs/maps/images/screen_popup_in_game.png b/docs/maps/images/screen_popup_in_game.png deleted file mode 100644 index 6378bec1..00000000 Binary files a/docs/maps/images/screen_popup_in_game.png and /dev/null differ diff --git a/docs/maps/images/screen_popup_tiled.png b/docs/maps/images/screen_popup_tiled.png deleted file mode 100644 index 66a42f8f..00000000 Binary files a/docs/maps/images/screen_popup_tiled.png and /dev/null differ diff --git a/docs/maps/images/script_property.png b/docs/maps/images/script_property.png deleted file mode 100644 index a374ff11..00000000 Binary files a/docs/maps/images/script_property.png and /dev/null differ diff --git a/docs/maps/images/start_kit_start_screen.png b/docs/maps/images/start_kit_start_screen.png deleted file mode 100644 index fd1a63f2..00000000 Binary files a/docs/maps/images/start_kit_start_screen.png and /dev/null differ diff --git a/docs/maps/images/text-object.png b/docs/maps/images/text-object.png deleted file mode 100644 index 7e5b3da7..00000000 Binary files a/docs/maps/images/text-object.png and /dev/null differ diff --git a/docs/maps/images/tile-entry-point.png b/docs/maps/images/tile-entry-point.png deleted file mode 100644 index 45c36d2d..00000000 Binary files a/docs/maps/images/tile-entry-point.png and /dev/null differ diff --git a/docs/maps/images/trigger_event.png b/docs/maps/images/trigger_event.png deleted file mode 100644 index b695476a..00000000 Binary files a/docs/maps/images/trigger_event.png and /dev/null differ diff --git a/docs/maps/images/trigger_message.png b/docs/maps/images/trigger_message.png deleted file mode 100644 index a116294b..00000000 Binary files a/docs/maps/images/trigger_message.png and /dev/null differ diff --git a/docs/maps/images/website_allowapi_property.png b/docs/maps/images/website_allowapi_property.png deleted file mode 100644 index 25e4df66..00000000 Binary files a/docs/maps/images/website_allowapi_property.png and /dev/null differ diff --git a/docs/maps/images/website_url_property.png b/docs/maps/images/website_url_property.png deleted file mode 100644 index 331135b8..00000000 Binary files a/docs/maps/images/website_url_property.png and /dev/null differ diff --git a/docs/maps/images/youtube.jpg b/docs/maps/images/youtube.jpg deleted file mode 100644 index 3dc6c15b..00000000 Binary files a/docs/maps/images/youtube.jpg and /dev/null differ diff --git a/docs/maps/index.md b/docs/maps/index.md deleted file mode 100644 index d018dd3c..00000000 --- a/docs/maps/index.md +++ /dev/null @@ -1,128 +0,0 @@ -{.section-title.accent.text-primary} -# Create your map - -## Tools you will need - -In order to build your own map for WorkAdventure, you need: - -* the [Tiled editor](https://www.mapeditor.org/) software -* "tiles" (i.e. images) to create your map -* a web-server to serve your map - -WorkAdventure comes with a "map starter kit" that we recommend using to start designing your map quickly. It contains **a good default tileset** for building an office and it proposes to **use Github static pages as a web-server** which is both free and performant. It also comes with a local webserver for testing purpose and with Typescript support (if you are looking to use the [map scripting API]({{url('/map-building/scripting')}}). - -{.alert.alert-info} -If you are looking to host your maps on your own webserver, be sure to read the [Self-hosting your map](hosting.md) guide. - -[Building your map - Create your map](https://www.youtube.com/watch?v=lu1IZgBJJD4) - -## Getting started - -Start by [creating a GitHub account](https://github.com/join) if you don't already have one. - -Then, go to the [Github map starter kit repository page](https://github.com/thecodingmachine/workadventure-map-starter-kit) and click the **"Use this template"** button. - -
- -
The "Use this template" button
-
- -You will be prompted to enter a repository name for your map. - -
- -
The "create a new repository" page
-
- -**Make sure to check the "Include all branches" checkbox, otherwise the Github Pages deployment process will not be setup automatically.** - -If you miss that step, don't worry, you can always fix that by clicking on the **Settings tab** of your repository and scroll down to the **GitHub Pages** section. Then select the **gh-pages** branch. It might already be selected, but please be sure to click on it nonetheless (otherwise GitHub will not enable GitHub pages). - -{.alert.alert-info} -If you only see a "master" branch and if the **gh-pages** branch does not appear here, simply wait a few minutes and refresh your page. When you created the project, Github Actions triggered a job that is in charge of creating the **gh-pages** branch. Maybe the job is not finished yet. - -
- -
The GitHub pages configuration section
-
- -Wait a few minutes... Github will deploy a new website with the content of the repository. The address of the website is visible in the "GitHub Pages" section. - -
- -
Your website is ready!
-
- -Click on the link. You should be redirected directly to WorkAdventure, on your map! - -## Customizing your map - -Your map is now up and online, but this is still the demo map from the starter kit. You need to customize it. - -### Cloning the map - -Start by cloning the map. If you are used to Git and GitHub, simply clone the map to your computer using your preferred tool and [jump to the next chapter](#loading-the-map-in-tiled). - -If you are new to Git, cloning the map means downloading the map to your computer. To do this, you will need Git, or a Git compatible tool. Our advice is to use [GitHub Desktop](https://desktop.github.com/). We recommend you take some time mastering the notion of pull / commit / push as this will make uploading your maps really easier. - -As an (easier) alternative, you can simply use the "Export" button to download the code of the map in a big Zip file. When you want to upload your work again, you will simply drag'n'drop your files in the GitHub website. - -### Loading the map in Tiled - -The sample map is in the file `map.json`. You can load this file in [Tiled](https://www.mapeditor.org/). - -Now, it's up to you to edit the map and write your own map. - -Some resources regarding Tiled: - -* [Tiled documentation](https://doc.mapeditor.org/en/stable/manual/introduction/) -* [Tiled video tutorials](https://www.gamefromscratch.com/post/2015/10/14/Tiled-Map-Editor-Tutorial-Series.aspx) - -### Testing your map locally - -In order to test your map, you need a webserver to host your map. The "map starter kit" comes with a local webserver that you can use to test your map. - -In order to start the webserver, you will need [Node.JS](https://nodejs.org/en/). When it is downloaded, open your command line and from the directory of the map, run this command: - - $ npm install - -This will install the local webserver. - - $ npm run start - -This command will start the webserver and open the welcome page. You should see a page looking like this: - -
- -
The welcome page of the "map start kit"
-
- -From here, you simply need to click the "Test this map" button to test your map in WorkAdventure. - -{.alert.alert-warning} -The local web server can only be used to test your map locally. In particular, the link will only work on your computer. You cannot share it with other people. - -### Pushing the map - -When your changes are ready, you need to "commit" and "push" (i.e. "upload") the changes back to GitHub. Just wait a few minutes, and your map will be propagated automatically to the GitHub pages web-server. - -## Testing your map - -To test your map, you need to find its URL. There are 2 kinds of URLs in WorkAdventure: - -* Test URLs are in the form `https://play.workadventu.re/_/[instance]/[server]/[path to map]` -* Registered URLs are in the form `https://play.workadventu.re/@/[organization]/[world]/[map]` - -Assuming your JSON map is hosted at "`https://myuser.github.io/myrepo/map.json`", then you can browse your map at "`https://play.workadventu.re/_/global/myuser.github.io/myrepo/map.json`". Here, "global" is a name of an "instance" of your map. You can put anything instead of "global" here. People on the same instance of the map can see each others. If 2 users use 2 different instances, they are on the same map, but in 2 parallel universes. They cannot see each other. - -This will connect you to a "public" instance. Anyone can come and connect to a public instance. If you want to manage invitations, or to perform some moderation, you will need to create a "private" instance. Private instances are available in "pro" accounts. - -
-
-

Need some help?

-

WorkAdventure is a constantly evolving project and there is plenty of room for improvement regarding map editing.

-

If you are facing any troubles, do not hesitate to open an "issue" in the - GitHub WorkAdventure account. -

-
-
diff --git a/docs/maps/meeting-rooms.md b/docs/maps/meeting-rooms.md deleted file mode 100644 index 88db1621..00000000 --- a/docs/maps/meeting-rooms.md +++ /dev/null @@ -1,82 +0,0 @@ -{.section-title.accent.text-primary} -# Meeting rooms - -[Building your map - Meeting room](https://www.youtube.com/watch?v=cN9VMWHN0eo) - -## Opening a Jitsi meet when walking on the map - -On your map, you can define special zones (meeting rooms) that will trigger the opening of a Jitsi meet. When a player will pass over these zones, a Jitsi meet will open (as an iframe on the right side of the screen) - -In order to create Jitsi meet zones: - -* You must create a specific layer. -* In layer properties, you MUST add a "`jitsiRoom`" property (of type "`string`"). The value of the property is the name of the room in Jitsi. Note: the name of the room will be "slugified" and prepended with the name of the instance of the map (so that different instances of the map have different rooms) -* You may also use "jitsiWidth" property (of type "number" between 0 and 100) to control the width of the iframe containing the meeting room. - -## Triggering of the "Jitsi meet" action - -By default, Jitsi meet will open when a user enters the zone defined on the map. - -It is however possible to trigger Jitsi only on user action. You can do this with the `jitsiTrigger` property. - -If you set `jitsiTrigger: onaction`, when the user walks on the layer, an alert message will be displayed at the bottom of the screen: - -
- -
Jitsi meet will only open if the user clicks Space
-
- -If you set `jitsiTriggerMessage: your message action` you can edit alert message displayed. If is not defined, the default message displayed is 'Press on SPACE to enter in jitsi meet room'. - -## Customizing your "Jitsi meet" - -Your Jitsi meet experience can be customized using Jitsi specific config options. The `jitsiConfig` and `jitsiInterfaceConfig` properties can be used on the Jitsi layer to change the way Jitsi looks and behaves. Those 2 properties are accepting a JSON string. - -For instance, use `jitsiConfig: { "startWithAudioMuted": true }` to automatically mute the microphone when someone enters a room. Or use `jitsiInterfaceConfig: { "DEFAULT_BACKGROUND": "#77ee77" }` to change the background color of Jitsi. - -The `jitsiConfig` property will override the Jitsi [config.js](https://github.com/jitsi/jitsi-meet/blob/master/config.js) file - -The `jitsiInterfaceConfig` property will override the Jitsi [interface_config.js](https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js) file - -
If your customizations are not working: -
    -
  • First, check that the JSON you are entering is valid. Take a look at the console in your browser. If the JSON string is invalid, you should see a warning.
  • -
  • Then, check that the JSON you are using is matching the version of Jitsi used.
  • -
-
- -## Granting moderator controls in Jitsi - -{.alert.alert-info} -Moderator controls are linked to member tags. You need a pro account to edit member tags. - -You can grant moderator rights to some of your members. Jitsi moderators can: - -* Publish a Jitsi meeting on Youtube Live (you will need a Youtube Live account) -* Record a meeting to Dropbox (you will need a Dropbox account) -* Mute someone -* Mute everybody expect one speaker -* Kick users out of the meeting - -In order to grant moderator rights to a given user, you can add a `jitsiRoomAdminTag` property to your Jitsi layer. For instance, if you write a property: - - jitsiRoomAdminTag: speaker - -then, any of your member with the `speaker` tag will be automatically granted moderator rights over this Jitsi instance. - -You can read more about [managing member tags in the admin documentation](/admin-guide/manage-members). - -## Using another Jitsi server - -WorkAdventure usually comes with a default Jitsi meet installation. If you are using the online version at `workadventu.re`, we are handling a Jitsi meet cluster for you. If you are running the self-hosted version of WorkAdventure, the administrator probably set up a Jitsi meet instance too. - -You have the possibility, in your map, to override the Jitsi meet instance that will be used by default. This can be useful for regulatory reasons. Maybe your company wants to keep control on the video streams and therefore, wants to self-host a Jitsi instance? Or maybe you want to use a very special configuration or very special version of Jitsi? - -Use the `jitsiUrl` property to in the Jitsi layer to specify the Jitsi instance that should be used. Beware, `jitsiUrl` takes in parameter a **domain name**, without the protocol. So you should use: -`jitsiUrl: meet.jit.si` -and not -`jitsiUrl: https://meet.jit.si` - -{.alert.alert-info} -When you use `jitsiUrl`, the targeted Jitsi instance must be public. You cannot use moderation features or the JWT -tokens authentication with maps configured using the `jitsiUrl` property. diff --git a/docs/maps/menu.php b/docs/maps/menu.php deleted file mode 100644 index c8afc2c0..00000000 --- a/docs/maps/menu.php +++ /dev/null @@ -1,172 +0,0 @@ - 'Getting started', - 'url' => '/map-building/', - 'markdown' => 'maps.index', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/index.md', - ], - [ - 'title' => 'WorkAdventure maps', - 'url' => '/map-building/wa-maps.md', - 'markdown' => 'maps.wa-maps', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/wa-maps.md', - ], - [ - 'title' => 'Entries and exits', - 'url' => '/map-building/entry-exit.md', - 'markdown' => 'maps.entry-exit', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/entry-exit.md', - ], - [ - 'title' => 'Opening a website', - 'url' => '/map-building/opening-a-website.md', - 'markdown' => 'maps.opening-a-website', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/opening-a-website.md', - ], - [ - 'title' => 'Meeting rooms', - 'url' => '/map-building/meeting-rooms.md', - 'markdown' => 'maps.meeting-rooms', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/meeting-rooms.md', - ], - [ - 'title' => 'Special zones', - 'url' => '/map-building/special-zones.md', - 'markdown' => 'maps.special-zones', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/special-zones.md', - ], - [ - 'title' => 'Animations', - 'url' => '/map-building/animations.md', - 'markdown' => 'maps.animations', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/animations.md', - ], - [ - 'title' => 'Integrated websites', - 'url' => '/map-building/website-in-map.md', - 'markdown' => 'maps.website-in-map', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/website-in-map.md', - ], - [ - 'title' => 'Camera', - 'url' => '/map-building/camera.md', - 'markdown' => 'maps.camera', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/camera.md', - ], - [ - 'title' => 'Variables', - 'url' => '/map-building/variables.md', - 'markdown' => 'maps.variables', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/variables.md', - ], - [ - 'title' => 'Self-hosting your map', - 'url' => '/map-building/hosting.md', - 'markdown' => 'maps.hosting', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/hosting.md', - ], - $extraMenu, - [ - 'title' => 'Scripting maps', - 'url' => '/map-building/scripting.md', - 'markdown' => 'maps.scripting', - 'children' => [ - [ - 'title' => 'Using Typescript', - 'url' => '/map-building/using-typescript.md', - 'markdown' => 'maps.using-typescript', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/using-typescript.md', - ], - [ - 'title' => 'API Reference', - 'url' => '/map-building/api-reference', - 'markdown' => 'maps.api-reference', - 'collapse' => true, - 'children' => [ - [ - 'title' => 'Initialization', - 'url' => '/map-building/api-start.md', - 'markdown' => 'maps.api-start', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-start.md', - ], - [ - 'title' => 'Navigation', - 'url' => '/map-building/api-nav.md', - 'markdown' => 'maps.api-nav', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-nav.md', - ], - [ - 'title' => 'Chat', - 'url' => '/map-building/api-chat.md', - 'markdown' => 'maps.api-chat', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-chat.md', - ], - [ - 'title' => 'Room', - 'url' => '/map-building/api-room.md', - 'markdown' => 'maps.api-room', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-room.md', - ], - [ - 'title' => 'State', - 'url' => '/map-building/api-state.md', - 'markdown' => 'maps.api-state', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-state.md', - ], - [ - 'title' => 'Player', - 'url' => '/map-building/api-player.md', - 'markdown' => 'maps.api-player', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-player.md', - ], - [ - 'title' => 'UI', - 'url' => '/map-building/api-ui.md', - 'markdown' => 'maps.api-ui', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-ui.md', - ], - [ - 'title' => 'Sound', - 'url' => '/map-building/api-sound.md', - 'markdown' => 'maps.api-sound', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-sound.md', - ], - [ - 'title' => 'Controls', - 'url' => '/map-building/api-controls.md', - 'markdown' => 'maps.api-controls', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-controls.md', - ], - [ - 'title' => 'Camera', - 'url' => '/map-building/api-camera.md', - 'markdown' => 'maps.api-camera', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-camera.md', - ], - [ - 'title' => 'Deprecated', - 'url' => '/map-building/api-deprecated.md', - 'markdown' => 'maps.api-deprecated', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/api-deprecated.md', - ], - ] - ], - $extraUtilsMenu, - [ - 'title' => 'Scripting internals', - 'url' => '/map-building/scripting-internals.md', - 'markdown' => 'maps.scripting-internals', - 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/scripting-internals.md', - ], - ] - ], - [ - 'title' => 'Troubleshooting', - 'url' => '/map-building/troubleshooting', - 'view' => 'content.map.troubleshooting' - ], -]; diff --git a/docs/maps/opening-a-website.md b/docs/maps/opening-a-website.md deleted file mode 100644 index a84bde30..00000000 --- a/docs/maps/opening-a-website.md +++ /dev/null @@ -1,83 +0,0 @@ -{.section-title.accent.text-primary} -# Opening a website when walking on the map - -[Building your map - Opening a website](https://www.youtube.com/watch?v=Me8cu5lLN3A) - -## The openWebsite property - -On your map, you can define special zones. When a player will pass over these zones, a website will open (as an iframe -on the right side of the screen) - -In order to create a zone that opens websites: - -* You must create a specific layer. -* In layer properties, you MUST add a "`openWebsite`" property (of type "`string`"). The value of the property is the URL of the website to open (the URL must start with "https://") -* You may also use "`openWebsiteWidth`" property (of type "`int`" or "`float`" between 0 and 100) to control the width of the iframe. -* You may also use "`openTab`" property (of type "`string`") to open in a new tab instead. - -{.alert.alert-warning} -A website can explicitly forbid another website from loading it in an iFrame using -the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). - -## Integrating a Youtube video - -A common use case is to use `openWebsite` to open a Youtube video. - -The global Youtube page cannot be embedded into an iFrame (it has the `X-Frame-Options` HTTP header). - -To embed a Youtube video, be sure to **use the "embed" link**. You can get this link be clicking "Share > Embed" in Youtube. - -
- -
Find the URL of your video in the "embed Video" HTML snippet on Youtube
-
- -
- -
Put this URL in the "openWebsite" property
-
- -### Triggering of the "open website" action - -By default, the iFrame will open when a user enters the zone defined on the map. - -It is however possible to trigger the iFrame only on user action. You can do this with the `openWebsiteTrigger` property. - -If you set `openWebsiteTrigger: onaction`, when the user walks on the layer, an alert message will be displayed at the bottom of the screen: - -
- -
The iFrame will only open if the user clicks Space
-
- -If you set `openWebsiteTriggerMessage: your message action` you can edit alert message displayed. If is not defined, the default message displayed is 'Press on SPACE to open the web site'. - -If you set `openWebsiteTrigger: onicon`, when the user walks on the layer, an icon will be displayed at the bottom of the screen: - -
- -
The iFrame will only open if the user clicks on icon
-
- -### Setting the iFrame "allow" attribute - -By default, iFrames have limited rights in browsers. For instance, they cannot put their content in fullscreen, they cannot start your webcam, etc... - -If you want to grant additional access rights to your iFrame, you should use the `openWebsitePolicy` property. The value of this property will be directly used for the [`allow` atttribute of your iFrame](https://developer.mozilla.org/en-US/docs/Web/HTTP/Feature_Policy/Using_Feature_Policy#the_iframe_allow_attribute). - -For instance, if you want an iFrame to be able to go in fullscreen, you will use the property `openWebsitePolicy: fullscreen` - -
- -
The generated iFrame will have the allow attribute set to: <iframe allow="fullscreen">
-
- -### Open a Jitsi with a co-website - -Cowebsites allow you to have several sites open at the same time. - -If you want to open a Jitsi and another page it's easy! - -You have just to [add a Jitsi to the map](meeting-rooms.md) and [add a co-website](opening-a-website.md#the-openwebsite-property) on the same layer. - -It's done! diff --git a/docs/maps/scripting-internals.md b/docs/maps/scripting-internals.md deleted file mode 100644 index 65bd9850..00000000 --- a/docs/maps/scripting-internals.md +++ /dev/null @@ -1,62 +0,0 @@ -{.section-title.accent.text-primary} -# Scripting internals - -Internally, scripts are always loaded inside `iframes`. - -You can load a script: - -1. Using the [`script` property in your map properties](scripting.md#adding-a-script-in-the-map) -2. or from an iframe [opened as a co-website](scripting.md#adding-a-script-in-an-iframe) or [embedded in the map](website-in-map.md#allowing-the-scripting-api-in-your-iframe) - -## Script restrictions - -If you load a script using the `script` property in your map properties (solution 1), you need to understand that -WorkAdventure will generate an iframe, and will load the script inside this iframe. - -Things you should know: - -{.alert.alert-warning} -The [iframe is sandboxed](https://blog.dareboost.com/en/2015/07/securing-iframe-sandbox-attribute/) - -This means that the iframe is generated with: - -``` -