Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
9364cb3b12 | |||
60606947ab | |||
61c6b9dacb | |||
f3fdcba16a |
|
@ -1,2 +0,0 @@
|
||||||
**/node_modules/**
|
|
||||||
**/Dockerfile
|
|
|
@ -1,31 +1 @@
|
||||||
DEBUG_MODE=false
|
DEBUG_MODE=false
|
||||||
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=
|
|
||||||
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=
|
|
164
.github/workflows/build-and-deploy.yml
vendored
164
.github/workflows/build-and-deploy.yml
vendored
|
@ -1,13 +1,7 @@
|
||||||
name: Build, push and deploy Docker image
|
name: Build, push and deploy Docker image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
- push
|
||||||
branches: [master, develop]
|
|
||||||
release:
|
|
||||||
types: [created]
|
|
||||||
pull_request:
|
|
||||||
types: [ labeled, synchronize ]
|
|
||||||
|
|
||||||
|
|
||||||
# Enables BuildKit
|
# Enables BuildKit
|
||||||
env:
|
env:
|
||||||
|
@ -16,7 +10,7 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build-front:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -26,21 +20,21 @@ jobs:
|
||||||
|
|
||||||
|
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@3.1.0
|
- uses: rlespinasse/github-slug-action@master
|
||||||
|
|
||||||
- name: "Build and push front image"
|
- name: "Build and push front image"
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
dockerfile: front/Dockerfile
|
dockerfile: front/Dockerfile
|
||||||
path: ./
|
path: front/
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-front
|
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
|
add_git_labels: true
|
||||||
|
|
||||||
build-back:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -49,156 +43,60 @@ jobs:
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@3.1.0
|
- uses: rlespinasse/github-slug-action@master
|
||||||
|
|
||||||
- name: "Build and push back image"
|
- name: "Build and push back image"
|
||||||
uses: docker/build-push-action@v1
|
uses: docker/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
dockerfile: back/Dockerfile
|
dockerfile: back/Dockerfile
|
||||||
path: ./
|
path: back/
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-back
|
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') }}
|
|
||||||
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: pusher/Dockerfile
|
|
||||||
path: ./
|
|
||||||
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 }}
|
|
||||||
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:
|
|
||||||
|
|
||||||
- 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 front image"
|
|
||||||
uses: docker/build-push-action@v1
|
|
||||||
with:
|
|
||||||
dockerfile: maps/Dockerfile
|
|
||||||
path: maps/
|
|
||||||
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 }}
|
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
deeploy:
|
deeploy:
|
||||||
needs:
|
needs:
|
||||||
- build-front
|
- build-front
|
||||||
- build-back
|
- build-back
|
||||||
- build-pusher
|
|
||||||
- build-maps
|
|
||||||
- build-uploader
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@3.1.0
|
- uses: rlespinasse/github-slug-action@1.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"
|
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: thecodingmachine/deeployer-action@master
|
uses: thecodingmachine/deeployer@master
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
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:
|
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
|
- name: Add a comment in PR
|
||||||
uses: unsplash/comment-on-pr@v1.2.0
|
uses: unsplash/comment-on-pr@v1.2.0
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
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 http://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||||
|
check_for_duplicate_msg: true
|
||||||
|
|
||||||
|
- name: Run Cypress tests
|
||||||
|
uses: cypress-io/github-action@v1
|
||||||
|
env:
|
||||||
|
CYPRESS_BASE_URL: http://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||||
|
with:
|
||||||
|
env: host=${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com,port=80
|
||||||
|
spec: cypress/integration/spec.js
|
||||||
|
wait-on: http://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||||
|
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"
|
||||||
|
|
26
.github/workflows/cleanup.yml
vendored
26
.github/workflows/cleanup.yml
vendored
|
@ -1,26 +0,0 @@
|
||||||
name: Cleanup images and environments
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [ closed ]
|
|
||||||
|
|
||||||
# Enables BuildKit
|
|
||||||
env:
|
|
||||||
DOCKER_BUILDKIT: 1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
delete_namespace:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# Create a slugified value of the branch
|
|
||||||
- uses: rlespinasse/github-slug-action@3.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 }}
|
|
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
|
@ -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
|
|
110
.github/workflows/continuous_integration.yml
vendored
110
.github/workflows/continuous_integration.yml
vendored
|
@ -3,13 +3,11 @@
|
||||||
name: "Continuous Integration"
|
name: "Continuous Integration"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
- "pull_request"
|
||||||
branches:
|
- "push"
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
continuous-integration-front:
|
continuous-integration-front:
|
||||||
name: "Continuous Integration Front"
|
name: "Continuous Integration Front"
|
||||||
|
|
||||||
|
@ -22,103 +20,22 @@ jobs:
|
||||||
- name: "Setup NodeJS"
|
- name: "Setup NodeJS"
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: '12.x'
|
||||||
|
|
||||||
- name: Install Protoc
|
|
||||||
uses: arduino/setup-protoc@v1
|
|
||||||
with:
|
|
||||||
version: '3.x'
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: yarn install
|
run: yarn install
|
||||||
working-directory: "front"
|
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"
|
- name: "Build"
|
||||||
run: yarn run build
|
run: yarn run build
|
||||||
env:
|
env:
|
||||||
PUSHER_URL: "//localhost:8080"
|
API_URL: "http://localhost:8080"
|
||||||
ADMIN_URL: "//localhost:80"
|
|
||||||
working-directory: "front"
|
|
||||||
|
|
||||||
- name: "Svelte check"
|
|
||||||
run: yarn run svelte-check
|
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
- name: "Lint"
|
- name: "Lint"
|
||||||
run: yarn run lint
|
run: yarn run lint
|
||||||
working-directory: "front"
|
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:
|
continuous-integration-back:
|
||||||
name: "Continuous Integration Back"
|
name: "Continuous Integration Back"
|
||||||
|
|
||||||
|
@ -133,23 +50,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '12.x'
|
||||||
|
|
||||||
- name: Install Protoc
|
|
||||||
uses: arduino/setup-protoc@v1
|
|
||||||
with:
|
|
||||||
version: '3.x'
|
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
run: yarn install
|
run: yarn install
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
|
||||||
- name: "Install messages dependencies"
|
|
||||||
run: yarn install
|
|
||||||
working-directory: "messages"
|
|
||||||
|
|
||||||
- name: "Build proto messages"
|
|
||||||
run: yarn run proto && yarn run copy-to-back
|
|
||||||
working-directory: "messages"
|
|
||||||
|
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: yarn run tsc
|
run: yarn run tsc
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
@ -162,7 +66,3 @@ jobs:
|
||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
|
||||||
- name: "Prettier"
|
|
||||||
run: yarn run pretty-check
|
|
||||||
working-directory: "back"
|
|
||||||
|
|
||||||
|
|
126
.github/workflows/end_to_end_tests.yml
vendored
126
.github/workflows/end_to_end_tests.yml
vendored
|
@ -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 }}
|
|
73
.github/workflows/push-to-npm.yml
vendored
73
.github/workflows/push-to-npm.yml
vendored
|
@ -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' }}
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,11 +1,4 @@
|
||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
.vagrant
|
.vagrant
|
||||||
Vagrantfile
|
Vagrantfile
|
||||||
docker-compose.override.yaml
|
|
||||||
*.DS_Store
|
|
||||||
maps/yarn.lock
|
|
||||||
maps/dist/computer.js
|
|
||||||
maps/dist/computer.js.map
|
|
||||||
node_modules
|
|
||||||
_
|
|
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
_
|
|
|
@ -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
|
|
||||||
)
|
|
166
CHANGELOG.md
166
CHANGELOG.md
|
@ -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<number>` 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<void>` 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<ITiledMap>` 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<void>` to set a variable (across the room, for all users)
|
|
||||||
- Use `WA.state.onVariableChange(key: string): Observable<unknown>` 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
|
|
113
CONTRIBUTING.md
113
CONTRIBUTING.md
|
@ -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.
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 16 KiB |
BIN
README-MAP.png
BIN
README-MAP.png
Binary file not shown.
Before Width: | Height: | Size: 60 KiB |
47
README.md
47
README.md
|
@ -1,48 +1,47 @@
|
||||||
![](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)
|
# Work Adventure
|
||||||
![WorkAdventure office image](README-MAP.png)
|
|
||||||
|
|
||||||
Live demo [here](https://play.workadventu.re/@/tcm/workadventure/wa-village).
|
## Work in progress
|
||||||
|
|
||||||
# WorkAdventure
|
Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a
|
||||||
|
|
||||||
WorkAdventure is a web-based collaborative workspace presented in the form of a
|
|
||||||
16-bit video game.
|
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
|
## Getting started
|
||||||
|
|
||||||
Check out resources developed by the WorkAdventure community at [awesome-workadventure](https://github.com/workadventure/awesome-workadventure)
|
|
||||||
|
|
||||||
## Setting up a development environment
|
|
||||||
|
|
||||||
Install Docker.
|
Install Docker.
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```
|
```
|
||||||
cp .env.template .env
|
docker-compose up
|
||||||
docker-compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The environment will start.
|
The environment will start.
|
||||||
|
|
||||||
You should now be able to browse to http://play.workadventure.localhost/ and see the application.
|
You should now be able to browse to http://workadventure.localhost/ and see the application.
|
||||||
You can view the dashboard at http://workadventure.localhost:8080/
|
|
||||||
|
|
||||||
Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
|
Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
|
||||||
|
|
||||||
**/etc/hosts**
|
**/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``.
|
## Designing a map
|
||||||
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.
|
|
||||||
|
If you want to design your own map, you can use [Tiled](https://www.mapeditor.org/).
|
||||||
|
|
||||||
|
A few things to notice:
|
||||||
|
|
||||||
|
- your map can have as many layers as your want
|
||||||
|
- your map MUST contain a layer named "floorLayer" of type "objectgroup" that represents the layer on which characters will be drawn.
|
||||||
|
|
||||||
|
![](doc/images/tiled_screenshot_1.png)
|
||||||
|
|
||||||
### MacOS developers, your environment with Vagrant
|
### MacOS developers, your environment with Vagrant
|
||||||
|
|
||||||
|
@ -109,7 +108,5 @@ Vagrant destroy
|
||||||
* `Vagrant halt`: stop your VM Vagrant.
|
* `Vagrant halt`: stop your VM Vagrant.
|
||||||
* `Vagrant destroy`: delete your VM Vagrant.
|
* `Vagrant destroy`: delete your VM Vagrant.
|
||||||
|
|
||||||
## Setting up a production environment
|
## Features developed
|
||||||
|
You have more details of features developed in back [README.md](./back/README.md).
|
||||||
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.
|
|
||||||
|
|
20
SECURITY.md
20
SECURITY.md
|
@ -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.
|
|
|
@ -2,7 +2,7 @@
|
||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
# Box / OS
|
# Box / OS
|
||||||
VAGRANT_BOX = 'bento/ubuntu-20.04'
|
VAGRANT_BOX = 'bento/ubuntu-19.10'
|
||||||
|
|
||||||
# VM User — 'vagrant' by default
|
# VM User — 'vagrant' by default
|
||||||
VM_USER = 'vagrant'
|
VM_USER = 'vagrant'
|
||||||
|
@ -58,7 +58,7 @@ Vagrant.configure(2) do |config|
|
||||||
apt-get update -y
|
apt-get update -y
|
||||||
apt-get install -y git
|
apt-get install -y git
|
||||||
apt-get install -y apt-transport-https
|
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 curl
|
||||||
apt-get install -y gnupg-agent
|
apt-get install -y gnupg-agent
|
||||||
apt-get install -y software-properties-common
|
apt-get install -y software-properties-common
|
||||||
|
@ -66,8 +66,8 @@ Vagrant.configure(2) do |config|
|
||||||
apt-key fingerprint 0EBFCD88
|
apt-key fingerprint 0EBFCD88
|
||||||
add-apt-repository \
|
add-apt-repository \
|
||||||
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
|
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
|
||||||
$(lsb_release -cs) \
|
$(lsb_release -cs) \
|
||||||
stable"
|
stable"
|
||||||
apt-get update -y
|
apt-get update -y
|
||||||
apt-get install -y docker-ce docker-ce-cli containerd.io
|
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
|
curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
|
|
@ -7,8 +7,7 @@
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"plugin:@typescript-eslint/eslint-recommended"
|
||||||
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
|
||||||
],
|
],
|
||||||
"globals": {
|
"globals": {
|
||||||
"Atomics": "readonly",
|
"Atomics": "readonly",
|
||||||
|
@ -17,15 +16,12 @@
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2018,
|
"ecmaVersion": 2018,
|
||||||
"sourceType": "module",
|
"sourceType": "module"
|
||||||
"project": "./tsconfig.json"
|
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@typescript-eslint"
|
"@typescript-eslint"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off"
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
|
||||||
"no-throw-literal": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1 +0,0 @@
|
||||||
src/Messages/generated
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 120,
|
|
||||||
"tabWidth": 4
|
|
||||||
}
|
|
|
@ -1,26 +1,9 @@
|
||||||
# protobuf build
|
FROM thecodingmachine/nodejs:12
|
||||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
|
|
||||||
WORKDIR /usr/src
|
|
||||||
COPY messages .
|
|
||||||
RUN yarn install && yarn proto
|
|
||||||
|
|
||||||
# typescript build
|
COPY --chown=docker:docker . .
|
||||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder2
|
|
||||||
WORKDIR /usr/src
|
|
||||||
COPY back/yarn.lock back/package.json ./
|
|
||||||
RUN yarn install
|
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
|
ENV NODE_ENV=production
|
||||||
RUN yarn install --production
|
|
||||||
|
|
||||||
USER node
|
CMD ["yarn", "run", "prod"]
|
||||||
CMD ["yarn", "run", "runprod"]
|
|
||||||
|
|
691
back/LICENSE.txt
691
back/LICENSE.txt
|
@ -1,691 +0,0 @@
|
||||||
NOTICE
|
|
||||||
This package contains software licensed under different
|
|
||||||
licenses, please refer to the NOTICE.txt file for further
|
|
||||||
information and LICENSES.txt for full license texts.
|
|
||||||
|
|
||||||
WorkAdventure Enterprise edition can be licensed independently from
|
|
||||||
the source under separate commercial terms.
|
|
||||||
|
|
||||||
The software ("Software") is developed and owned by TheCodingMachine
|
|
||||||
and is subject to the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, with the Commons Clause as follows:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license
|
|
||||||
for software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are
|
|
||||||
designed to take away your freedom to share and change the works. By
|
|
||||||
contrast, our General Public Licenses are intended to guarantee your
|
|
||||||
freedom to share and change all versions of a program--to make sure it
|
|
||||||
remains free software for all its users.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public
|
|
||||||
License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds
|
|
||||||
of works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further restriction,
|
|
||||||
you may remove that term. If a license document contains a further
|
|
||||||
restriction but permits relicensing or conveying under this License, you
|
|
||||||
may add to a covered work material governed by the terms of that license
|
|
||||||
document, provided that the further restriction does not survive such
|
|
||||||
relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have permission
|
|
||||||
to link or combine any covered work with a work licensed under version 3
|
|
||||||
of the GNU General Public License into a single combined work, and to
|
|
||||||
convey the resulting work. The terms of this License will continue to
|
|
||||||
apply to the part which is the covered work, but the work with which it is
|
|
||||||
combined will remain governed by version 3 of the GNU General Public
|
|
||||||
License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new
|
|
||||||
versions will be similar in spirit to the present version, but may differ
|
|
||||||
in detail to address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero
|
|
||||||
General Public License "or any later version" applies to it, you have
|
|
||||||
the option of following the terms and conditions either of that
|
|
||||||
numbered version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number
|
|
||||||
of the GNU Affero General Public License, you may choose any version
|
|
||||||
ever published by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that
|
|
||||||
proxy's public statement of acceptance of a version permanently
|
|
||||||
authorizes you to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
"Commons Clause" License Condition
|
|
||||||
|
|
||||||
The Software is provided to you by the Licensor under the License, as
|
|
||||||
defined below, subject to the following condition. Without limiting
|
|
||||||
other conditions in the License, the grant of rights under the License
|
|
||||||
will not include, and the License does not grant to you, the right to
|
|
||||||
Sell the Software. For purposes of the foregoing, "Sell" means
|
|
||||||
practicing any or all of the rights granted to you under the License
|
|
||||||
to provide to third parties, for a fee or other consideration,
|
|
||||||
a product or service that consists, entirely or substantially,
|
|
||||||
of the Software or the functionality of the Software. Any license
|
|
||||||
notice or attribution required by the License must also include
|
|
||||||
this Commons Cause License Condition notice.
|
|
61
back/README.md
Normal file
61
back/README.md
Normal file
|
@ -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)
|
|
@ -5,81 +5,41 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"dev": "ts-node-dev --respawn ./server.ts",
|
"dev": "ts-node-dev --respawn --transpileOnly ./server.ts",
|
||||||
"prod": "tsc && node --max-old-space-size=4096 ./dist/server.js",
|
"prod": "tsc && node ./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",
|
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||||
"lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts",
|
"lint": "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}'"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/thecodingmachine/workadventure.git"
|
"url": "git+https://github.com/thecodingmachine/workadventure.git"
|
||||||
},
|
},
|
||||||
"contributors": [
|
"author": "g.parant@thecodingmachine.com",
|
||||||
{
|
"license": "AGPL",
|
||||||
"name": "Grégoire Parant",
|
|
||||||
"email": "g.parant@thecodingmachine.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "David Négrier",
|
|
||||||
"email": "d.negrier@thecodingmachine.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Arthmaël Poly",
|
|
||||||
"email": "a.poly@thecodingmachine.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "SEE LICENSE IN LICENSE.txt",
|
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/thecodingmachine/workadventure/issues"
|
"url": "https://github.com/thecodingmachine/workadventure/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@workadventure/tiled-map-type-guard": "^1.0.3",
|
"@types/express": "^4.17.4",
|
||||||
"axios": "^0.21.2",
|
"@types/http-status-codes": "^1.2.0",
|
||||||
"busboy": "^0.3.1",
|
"@types/jsonwebtoken": "^8.3.8",
|
||||||
"circular-json": "^0.5.9",
|
"@types/socket.io": "^2.1.4",
|
||||||
"debug": "^4.3.1",
|
"@types/uuidv4": "^5.0.0",
|
||||||
"generic-type-guard": "^3.2.0",
|
"body-parser": "^1.19.0",
|
||||||
"google-protobuf": "^3.13.0",
|
"express": "^4.17.1",
|
||||||
"grpc": "^1.24.4",
|
"http-status-codes": "^1.4.0",
|
||||||
"ipaddr.js": "^2.0.1",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mkdirp": "^1.0.4",
|
"socket.io": "^2.3.0",
|
||||||
"prom-client": "^12.0.0",
|
"ts-node-dev": "^1.0.0-pre.44",
|
||||||
"query-string": "^6.13.3",
|
"typescript": "^3.8.3",
|
||||||
"redis": "^3.1.2",
|
|
||||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
|
||||||
"uuidv4": "^6.0.7"
|
"uuidv4": "^6.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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/jasmine": "^3.5.10",
|
||||||
"@types/jsonwebtoken": "^8.3.8",
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
"@types/mkdirp": "^1.0.1",
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
"@types/redis": "^2.8.31",
|
"eslint": "^6.8.0",
|
||||||
"@types/uuidv4": "^5.0.0",
|
"jasmine": "^3.5.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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,3 @@
|
||||||
// lib/server.ts
|
// lib/server.ts
|
||||||
import App from "./src/App";
|
import App from "./src/App";
|
||||||
import grpc from "grpc";
|
App.listen(8080, () => console.log(`Example app listening on port 8080!`))
|
||||||
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<IRoomManagerServer>(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);
|
|
|
@ -1,19 +1,39 @@
|
||||||
// lib/app.ts
|
// lib/app.ts
|
||||||
import { PrometheusController } from "./Controller/PrometheusController";
|
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
|
||||||
import { DebugController } from "./Controller/DebugController";
|
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
|
||||||
import { App as uwsApp } from "./Server/sifrr.server";
|
import express from "express";
|
||||||
|
import {Application, Request, Response} from 'express';
|
||||||
|
import bodyParser = require('body-parser');
|
||||||
|
import * as http from "http";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public app: uwsApp;
|
public app: Application;
|
||||||
public prometheusController: PrometheusController;
|
public server: http.Server;
|
||||||
private debugController: DebugController;
|
public ioSocketController: IoSocketController;
|
||||||
|
public authenticateController: AuthenticateController;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = new uwsApp();
|
this.app = express();
|
||||||
|
|
||||||
this.prometheusController = new PrometheusController(this.app);
|
//config server http
|
||||||
this.debugController = new DebugController(this.app);
|
this.config();
|
||||||
|
this.server = http.createServer(this.app);
|
||||||
|
|
||||||
|
//create controllers
|
||||||
|
this.ioSocketController = new IoSocketController(this.server);
|
||||||
|
this.authenticateController = new AuthenticateController(this.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add session user
|
||||||
|
private config(): void {
|
||||||
|
this.app.use(bodyParser.json());
|
||||||
|
this.app.use(bodyParser.urlencoded({extended: false}));
|
||||||
|
this.app.use(function (req: Request, res: Response, next) {
|
||||||
|
res.header("Access-Control-Allow-Origin", "*"); // update to match the domain you will make the request from
|
||||||
|
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
|
next();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new App().app;
|
export default new App().server;
|
39
back/src/Controller/AuthenticateController.ts
Normal file
39
back/src/Controller/AuthenticateController.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import {Application, Request, Response} from "express";
|
||||||
|
import Jwt from "jsonwebtoken";
|
||||||
|
import {BAD_REQUEST, OK} from "http-status-codes";
|
||||||
|
import {SECRET_KEY, ROOM} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||||
|
import { uuid } from 'uuidv4';
|
||||||
|
import {userManager} from "../Model/Users/UserManager";
|
||||||
|
|
||||||
|
export class AuthenticateController{
|
||||||
|
App : Application;
|
||||||
|
|
||||||
|
constructor(App : Application) {
|
||||||
|
this.App = App;
|
||||||
|
this.login();
|
||||||
|
this.getAllUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
//permit to login on application. Return token to connect on Websocket IO.
|
||||||
|
login(){
|
||||||
|
this.App.post("/login", (req: Request, res: Response) => {
|
||||||
|
let param = req.body;
|
||||||
|
if(!param.email){
|
||||||
|
return res.status(BAD_REQUEST).send({
|
||||||
|
message: "email parameter is empty"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//TODO check user email for The Coding Machine game
|
||||||
|
let user = userManager.createUser(param.email);
|
||||||
|
let token = Jwt.sign({email: user.email, userId: user.id}, SECRET_KEY, {expiresIn: '24h'});
|
||||||
|
return res.status(OK).send(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllUsers(){
|
||||||
|
this.App.get("/users", (req: Request, res: Response) => {
|
||||||
|
let users = userManager.getAllUsers();
|
||||||
|
return res.status(OK).send(users);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
import { HttpResponse } from "uWebSockets.js";
|
|
||||||
|
|
||||||
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", "*");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
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";
|
|
||||||
|
|
||||||
export class DebugController {
|
|
||||||
constructor(private App: App) {
|
|
||||||
this.getDump();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDump() {
|
|
||||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
(async () => {
|
|
||||||
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!");
|
|
||||||
}
|
|
||||||
|
|
||||||
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<unknown> = [];
|
|
||||||
for (const [setKey, setValue] of value.entries()) {
|
|
||||||
obj.push(setValue);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})().catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
res.writeStatus("500");
|
|
||||||
res.end("An error occurred");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
124
back/src/Controller/IoSocketController.ts
Normal file
124
back/src/Controller/IoSocketController.ts
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import socketIO = require('socket.io');
|
||||||
|
import {Socket} from "socket.io";
|
||||||
|
import * as http from "http";
|
||||||
|
import {MessageUserPosition} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
|
||||||
|
import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||||
|
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
||||||
|
import {SECRET_KEY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||||
|
import {ExtRooms, RefreshUserPositionFunction} from "../Model/Websocket/ExtRoom";
|
||||||
|
import {ExtRoomsInterface} from "_Model/Websocket/ExtRoomsInterface";
|
||||||
|
import {userManager} from "../Model/Users/UserManager";
|
||||||
|
|
||||||
|
export class IoSocketController{
|
||||||
|
Io: socketIO.Server;
|
||||||
|
constructor(server : http.Server) {
|
||||||
|
this.Io = socketIO(server);
|
||||||
|
|
||||||
|
// Authentication with token. it will be decoded and stored in the socket.
|
||||||
|
this.Io.use( (socket: Socket, next) => {
|
||||||
|
if (!socket.handshake.query || !socket.handshake.query.token) {
|
||||||
|
return next(new Error('Authentication error'));
|
||||||
|
}
|
||||||
|
Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => {
|
||||||
|
if (err) {
|
||||||
|
return next(new Error('Authentication error'));
|
||||||
|
}
|
||||||
|
(socket as ExSocketInterface).token = tokenDecoded;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ioConnection();
|
||||||
|
this.shareUsersPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
ioConnection() {
|
||||||
|
this.Io.on('connection', (socket: Socket) => {
|
||||||
|
/*join-rom event permit to join one room.
|
||||||
|
message :
|
||||||
|
userId : user identification
|
||||||
|
roomId: room identification
|
||||||
|
position: position of user in map
|
||||||
|
x: user x position on map
|
||||||
|
y: user y position on map
|
||||||
|
*/
|
||||||
|
socket.on('join-room', (message : string) => {
|
||||||
|
let messageUserPosition = this.hydrateMessageReceive(message);
|
||||||
|
if(messageUserPosition instanceof Error){
|
||||||
|
return socket.emit("message-error", JSON.stringify({message: messageUserPosition.message}))
|
||||||
|
}
|
||||||
|
|
||||||
|
//join user in room
|
||||||
|
socket.join(messageUserPosition.roomId);
|
||||||
|
|
||||||
|
// sending to all clients in room except sender
|
||||||
|
this.saveUserInformation((socket as ExSocketInterface), messageUserPosition);
|
||||||
|
|
||||||
|
//add function to refresh position user in real time.
|
||||||
|
let rooms = (this.Io.sockets.adapter.rooms as ExtRoomsInterface);
|
||||||
|
rooms.refreshUserPosition = RefreshUserPositionFunction;
|
||||||
|
rooms.refreshUserPosition(rooms, this.Io);
|
||||||
|
|
||||||
|
socket.to(messageUserPosition.roomId).emit('join-room', messageUserPosition.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('user-position', (message : string) => {
|
||||||
|
let messageUserPosition = this.hydrateMessageReceive(message);
|
||||||
|
if (messageUserPosition instanceof Error) {
|
||||||
|
return socket.emit("message-error", JSON.stringify({message: messageUserPosition.message}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// sending to all clients in room except sender
|
||||||
|
this.saveUserInformation((socket as ExSocketInterface), messageUserPosition);
|
||||||
|
|
||||||
|
//refresh position of all user in all rooms in real time
|
||||||
|
let rooms = (this.Io.sockets.adapter.rooms as ExtRoomsInterface)
|
||||||
|
rooms.refreshUserPosition(rooms, this.Io);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//permit to save user position in socket
|
||||||
|
saveUserInformation(socket : ExSocketInterface, message : MessageUserPosition){
|
||||||
|
socket.position = message.position;
|
||||||
|
socket.roomId = message.roomId;
|
||||||
|
socket.userId = message.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Hydrate and manage error
|
||||||
|
hydrateMessageReceive(message : string) : MessageUserPosition | Error{
|
||||||
|
try {
|
||||||
|
return new MessageUserPosition(message);
|
||||||
|
}catch (err) {
|
||||||
|
//TODO log error
|
||||||
|
return new Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** permit to share user position
|
||||||
|
** users position will send in event 'user-position'
|
||||||
|
** The data sent is an array with information for each user :
|
||||||
|
[
|
||||||
|
{
|
||||||
|
userId: <string>,
|
||||||
|
roomId: <string>,
|
||||||
|
position: {
|
||||||
|
x : <number>,
|
||||||
|
y : <number>,
|
||||||
|
direction: <string>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
**/
|
||||||
|
seTimeOutInProgress : any = null;
|
||||||
|
shareUsersPosition(){
|
||||||
|
//every 1/10 of seconds, emit the current list of events
|
||||||
|
setInterval(() => {
|
||||||
|
let userEvents = userManager.getEventList();
|
||||||
|
if (userEvents.length) {
|
||||||
|
this.Io.emit('user-position', JSON.stringify(userEvents));
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
import { App } from "../Server/sifrr.server";
|
|
||||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
|
||||||
import { register, collectDefaultMetrics } from "prom-client";
|
|
||||||
|
|
||||||
export class PrometheusController {
|
|
||||||
constructor(private App: App) {
|
|
||||||
collectDefaultMetrics({
|
|
||||||
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.end(register.metrics());
|
|
||||||
}
|
|
||||||
private metricsAsJSON(res: HttpResponse, req: HttpRequest): void {
|
|
||||||
res.writeHeader('Content-Type', 'application/json');
|
|
||||||
res.end(JSON.stringify(register.getMetricsAsJSON()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +1,7 @@
|
||||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
||||||
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
const ROOM = process.env.ROOM || "THECODINGMACHINE";
|
||||||
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 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";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MINIMUM_DISTANCE,
|
SECRET_KEY,
|
||||||
ADMIN_API_URL,
|
ROOM
|
||||||
ADMIN_API_TOKEN,
|
}
|
||||||
HTTP_PORT,
|
|
||||||
GRPC_PORT,
|
|
||||||
GROUP_RADIUS,
|
|
||||||
ALLOW_ARTILLERY,
|
|
||||||
CPU_OVERHEAT_THRESHOLD,
|
|
||||||
JITSI_URL,
|
|
||||||
JITSI_ISS,
|
|
||||||
SECRET_JITSI_KEY,
|
|
||||||
};
|
|
1
back/src/Messages/.gitignore
vendored
1
back/src/Messages/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
/generated/
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,657 +0,0 @@
|
||||||
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";
|
|
||||||
|
|
||||||
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<number, User>();
|
|
||||||
private readonly usersByUuid = new Map<string, User>();
|
|
||||||
private readonly groups = new Set<Group>();
|
|
||||||
private readonly admins = new Set<Admin>();
|
|
||||||
|
|
||||||
private itemsState = new Map<number, unknown>();
|
|
||||||
|
|
||||||
private readonly positionNotifier: PositionNotifier;
|
|
||||||
private versionNumber: number = 1;
|
|
||||||
private nextUserId: number = 1;
|
|
||||||
|
|
||||||
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
|
||||||
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
// A zone is 10 sprites wide.
|
|
||||||
this.positionNotifier = new PositionNotifier(
|
|
||||||
320,
|
|
||||||
320,
|
|
||||||
onEnters,
|
|
||||||
onMoves,
|
|
||||||
onLeaves,
|
|
||||||
onEmote,
|
|
||||||
onPlayerDetailsUpdated
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<GameRoom> {
|
|
||||||
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 getUsers(): Map<number, User> {
|
|
||||||
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);
|
|
||||||
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);
|
|
||||||
if (userObj === undefined) {
|
|
||||||
console.warn("User ", user.id, "does not belong to this game room! It should!");
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (userObj !== undefined) {
|
|
||||||
this.positionNotifier.leave(userObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify admins
|
|
||||||
for (const admin of this.admins) {
|
|
||||||
admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
|
||||||
return this.users.size === 0 && this.admins.size === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public updatePosition(user: User, userPosition: PointInterface): void {
|
|
||||||
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 {
|
|
||||||
if (user.silent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = user.group;
|
|
||||||
const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user);
|
|
||||||
|
|
||||||
if (group === undefined) {
|
|
||||||
// If the user is not part of a group:
|
|
||||||
// should he join a group?
|
|
||||||
|
|
||||||
// If the user is moving, don't try to join
|
|
||||||
if (user.getPosition().moving) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
this.groups.add(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let hasKickOutSomeone = false;
|
|
||||||
let followingMembers: User[] = [];
|
|
||||||
|
|
||||||
const previewNewGroupPosition = group.previewGroupPosition();
|
|
||||||
|
|
||||||
if (!previewNewGroupPosition) {
|
|
||||||
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) {
|
|
||||||
if (user.silent === silent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
user.silent = silent;
|
|
||||||
if (silent && user.group !== undefined) {
|
|
||||||
this.leaveGroup(user);
|
|
||||||
}
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
|
|
||||||
*
|
|
||||||
* @param user
|
|
||||||
*/
|
|
||||||
private leaveGroup(user: User): void {
|
|
||||||
const group = user.group;
|
|
||||||
if (group === undefined) {
|
|
||||||
throw new Error("The user is part of no group");
|
|
||||||
}
|
|
||||||
group.leave(user);
|
|
||||||
if (group.isEmpty()) {
|
|
||||||
group.destroy();
|
|
||||||
if (!this.groups.has(group)) {
|
|
||||||
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?
|
|
||||||
} else {
|
|
||||||
group.updatePosition();
|
|
||||||
//this.positionNotifier.updatePosition(group, group.getPosition(), oldPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks for the closest user that is:
|
|
||||||
* - close enough (distance <= minDistance)
|
|
||||||
* - not in a group
|
|
||||||
* - not silent
|
|
||||||
* OR
|
|
||||||
* - close enough to a group (distance <= groupRadius)
|
|
||||||
*/
|
|
||||||
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") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentUser === user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentUser.silent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
|
||||||
|
|
||||||
if (distance <= minimumDistanceFound && distance <= this.minDistance) {
|
|
||||||
minimumDistanceFound = distance;
|
|
||||||
matchingItem = currentUser;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.groups.forEach((group: Group) => {
|
|
||||||
if (group.isFull()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
|
||||||
if (distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
|
||||||
minimumDistanceFound = distance;
|
|
||||||
matchingItem = group;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return matchingItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
public setItemState(itemId: number, state: unknown) {
|
|
||||||
this.itemsState.set(itemId, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getItemsState(): Map<number, unknown> {
|
|
||||||
return this.itemsState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setVariable(name: string, value: string, user: User): Promise<void> {
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
|
||||||
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<MapDetailsData> {
|
|
||||||
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<ITiledMap> | 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<ITiledMap> {
|
|
||||||
if (!this.mapPromise) {
|
|
||||||
this.mapPromise = mapFetcher.fetchMap(this.mapUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.mapPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private variableManagerPromise: Promise<VariablesManager> | undefined;
|
|
||||||
private variableManagerLastLoad: Date | undefined;
|
|
||||||
|
|
||||||
private getVariableManager(): Promise<VariablesManager> {
|
|
||||||
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<Map<string, string>> {
|
|
||||||
const variablesManager = await this.getVariableManager();
|
|
||||||
return variablesManager.getVariablesForTags(tags);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,214 +0,0 @@
|
||||||
import { ConnectCallback, DisconnectCallback, GameRoom } 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";
|
|
||||||
|
|
||||||
export class Group implements Movable {
|
|
||||||
private static nextId: number = 1;
|
|
||||||
|
|
||||||
private id: number;
|
|
||||||
private users: Set<User>;
|
|
||||||
private x!: number;
|
|
||||||
private y!: number;
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
this.roomId = roomId;
|
|
||||||
this.users = new Set<User>();
|
|
||||||
this.id = Group.nextId;
|
|
||||||
Group.nextId++;
|
|
||||||
|
|
||||||
users.forEach((user: User) => {
|
|
||||||
this.join(user);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updatePosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
getUsers(): User[] {
|
|
||||||
return Array.from(this.users.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
getId(): number {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the barycenter of all users (i.e. the center of the group)
|
|
||||||
*/
|
|
||||||
getPosition(): PositionInterface {
|
|
||||||
return {
|
|
||||||
x: this.x,
|
|
||||||
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)
|
|
||||||
*/
|
|
||||||
updatePosition(): void {
|
|
||||||
const oldX = this.x;
|
|
||||||
const oldY = this.y;
|
|
||||||
|
|
||||||
// Let's compute the barycenter of all users.
|
|
||||||
const newPosition = this.previewGroupPosition();
|
|
||||||
|
|
||||||
if (!newPosition) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { x, y } = newPosition;
|
|
||||||
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
|
|
||||||
if (this.outOfBounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldX === undefined) {
|
|
||||||
this.currentZone = 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isFull(): boolean {
|
|
||||||
return this.users.size >= MAX_PER_GROUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this.users.size <= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
join(user: User): void {
|
|
||||||
// Broadcast on the right event
|
|
||||||
this.connectCallback(user, this);
|
|
||||||
this.users.add(user);
|
|
||||||
user.group = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
user.group = undefined;
|
|
||||||
|
|
||||||
if (this.users.size !== 0) {
|
|
||||||
this.updatePosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast on the right event
|
|
||||||
this.disconnectCallback(user, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Let's kick everybody out.
|
|
||||||
* Usually used when there is only one user left.
|
|
||||||
*/
|
|
||||||
destroy(): void {
|
|
||||||
if (!this.outOfBounds) {
|
|
||||||
this.positionNotifier.leave(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const user of this.users) {
|
|
||||||
this.leave(user);
|
|
||||||
}
|
|
||||||
this.wasDestroyed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A physical object that can be placed into a Zone
|
|
||||||
*/
|
|
||||||
export interface Movable {
|
|
||||||
getPosition(): PositionInterface;
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export interface PositionInterface {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
/**
|
|
||||||
* Tracks the position of every player on the map, and sends notifications to the players interested in knowing about the move
|
|
||||||
* (i.e. players that are looking at the zone the player is currently in)
|
|
||||||
*
|
|
||||||
* Internally, the PositionNotifier works with Zones. A zone is a square area of a map.
|
|
||||||
* Each player is in a given zone, and each player tracks one or many zones (depending on the player viewport)
|
|
||||||
*
|
|
||||||
* 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";
|
|
||||||
|
|
||||||
interface ZoneDescriptor {
|
|
||||||
i: number;
|
|
||||||
j: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function* getNearbyDescriptorsMatrix(middleZoneDescriptor: ZoneDescriptor): Generator<ZoneDescriptor> {
|
|
||||||
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!)
|
|
||||||
|
|
||||||
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
|
|
||||||
) {}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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 {
|
|
||||||
// Did we change zone?
|
|
||||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
|
||||||
const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y);
|
|
||||||
|
|
||||||
if (oldZoneDesc.i != newZoneDesc.i || oldZoneDesc.j != newZoneDesc.j) {
|
|
||||||
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
|
||||||
const newZone = this.getZone(newZoneDesc.i, newZoneDesc.j);
|
|
||||||
|
|
||||||
// Leave old zone
|
|
||||||
oldZone.leave(thing, newZone);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public leave(thing: Movable): void {
|
|
||||||
const oldPosition = thing.getPosition();
|
|
||||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
|
||||||
const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
|
||||||
oldZone.leave(thing, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getZone(i: number, j: number): Zone {
|
|
||||||
let zoneRow = this.zones[j];
|
|
||||||
if (zoneRow === undefined) {
|
|
||||||
zoneRow = new Array<Zone>();
|
|
||||||
this.zones[j] = zoneRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
let zone = this.zones[j][i];
|
|
||||||
if (zone === undefined) {
|
|
||||||
zone = new Zone(
|
|
||||||
this.onUserEnters,
|
|
||||||
this.onUserMoves,
|
|
||||||
this.onUserLeaves,
|
|
||||||
this.onEmote,
|
|
||||||
this.onPlayerDetailsUpdated,
|
|
||||||
i,
|
|
||||||
j
|
|
||||||
);
|
|
||||||
this.zones[j][i] = zone;
|
|
||||||
}
|
|
||||||
return zone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
|
||||||
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<User> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
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<PusherToBackMessage, ServerToClientMessage>;
|
|
||||||
|
|
||||||
export class User implements Movable {
|
|
||||||
public listenedZones: Set<Zone>;
|
|
||||||
public group?: Group;
|
|
||||||
private _following: User | undefined;
|
|
||||||
private followedBy: Set<User> = new Set<User>();
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
public id: number,
|
|
||||||
public readonly uuid: string,
|
|
||||||
public readonly IPAddress: 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
|
|
||||||
) {
|
|
||||||
this.listenedZones = new Set<Zone>();
|
|
||||||
|
|
||||||
this.positionNotifier.enter(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getPosition(): PointInterface {
|
|
||||||
return this.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setPosition(position: PointInterface): void {
|
|
||||||
const oldPosition = this.position;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
86
back/src/Model/Users/UserManager.ts
Normal file
86
back/src/Model/Users/UserManager.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import {uuid} from "uuidv4";
|
||||||
|
import {BehaviorSubject} from "rxjs";
|
||||||
|
|
||||||
|
export interface UserPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
position: UserPosition;
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
constructor(id: string, name: string, email: string, position: UserPosition) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.email = email;
|
||||||
|
this.position = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserPositionChangeEvent {
|
||||||
|
user: User;
|
||||||
|
deleted: boolean
|
||||||
|
added: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserManager {
|
||||||
|
|
||||||
|
private usersList: Map<string, User> = new Map();
|
||||||
|
|
||||||
|
private eventsList: UserPositionChangeEvent[] = [];
|
||||||
|
|
||||||
|
//todo add more parameters
|
||||||
|
createUser(email: string): User {
|
||||||
|
let userId = uuid();
|
||||||
|
let user = new User(userId, "toto", email, {x: 0, y: 0});
|
||||||
|
|
||||||
|
this.usersList.set(userId, user);
|
||||||
|
this.eventsList.push({added: true, deleted: false, user})
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUser(id: string): User {
|
||||||
|
let user = this.usersList.get(id);
|
||||||
|
if (!user) {
|
||||||
|
throw "Could not delete user with id "+id;
|
||||||
|
}
|
||||||
|
this.usersList.delete(id);
|
||||||
|
|
||||||
|
this.eventsList.push({added: false, deleted: true, user});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserPosition(id: string, userPosition: UserPosition): User {
|
||||||
|
let user = this.usersList.get(id);
|
||||||
|
if (!user) {
|
||||||
|
throw "Could not find user with id "+id;
|
||||||
|
}
|
||||||
|
user.position = userPosition;
|
||||||
|
this.usersList.set(id, user);
|
||||||
|
|
||||||
|
this.eventsList.push({added: false, deleted: false, user});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllUsers(): User[] {
|
||||||
|
let array = [];
|
||||||
|
for (const value of this.usersList.values()) {
|
||||||
|
array.push(value);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
//flush the list of events
|
||||||
|
getEventList(): UserPositionChangeEvent[] {
|
||||||
|
let events = this.eventsList;
|
||||||
|
this.eventsList = [];
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userManager = new UserManager();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
export interface CharacterLayer {
|
|
||||||
name: string;
|
|
||||||
url: string | undefined;
|
|
||||||
}
|
|
9
back/src/Model/Websocket/ExSocketInterface.ts
Normal file
9
back/src/Model/Websocket/ExSocketInterface.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import {Socket} from "socket.io";
|
||||||
|
import {PointInterface} from "./PointInterface";
|
||||||
|
|
||||||
|
export interface ExSocketInterface extends Socket {
|
||||||
|
token: any;
|
||||||
|
roomId: string;
|
||||||
|
userId: string;
|
||||||
|
position: PointInterface;
|
||||||
|
}
|
42
back/src/Model/Websocket/ExtRoom.ts
Normal file
42
back/src/Model/Websocket/ExtRoom.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import {ExtRoomsInterface} from "./ExtRoomsInterface";
|
||||||
|
import socketIO = require('socket.io');
|
||||||
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
|
|
||||||
|
export class ExtRooms implements ExtRoomsInterface{
|
||||||
|
userPositionMapByRoom: any;
|
||||||
|
refreshUserPosition: any;
|
||||||
|
|
||||||
|
[room: string]: SocketIO.Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
let RefreshUserPositionFunction = function(rooms : ExtRooms, Io: socketIO.Server){
|
||||||
|
let clients = Io.clients();
|
||||||
|
let socketsKey = Object.keys(Io.clients().sockets);
|
||||||
|
|
||||||
|
//create mapping with all users in all rooms
|
||||||
|
let mapPositionUserByRoom = new Map();
|
||||||
|
for(let i = 0; i < socketsKey.length; i++){
|
||||||
|
let socket = clients.sockets[socketsKey[i]] as ExSocketInterface;
|
||||||
|
if(!socket.position){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let data = {
|
||||||
|
userId : socket.userId,
|
||||||
|
roomId : socket.roomId,
|
||||||
|
position : socket.position,
|
||||||
|
};
|
||||||
|
let dataArray = <any>[];
|
||||||
|
if(mapPositionUserByRoom.get(data.roomId)){
|
||||||
|
dataArray = mapPositionUserByRoom.get(data.roomId);
|
||||||
|
dataArray.push(data);
|
||||||
|
}else{
|
||||||
|
dataArray = [data];
|
||||||
|
}
|
||||||
|
mapPositionUserByRoom.set(data.roomId, dataArray);
|
||||||
|
}
|
||||||
|
rooms.userPositionMapByRoom = Array.from(mapPositionUserByRoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
RefreshUserPositionFunction
|
||||||
|
}
|
6
back/src/Model/Websocket/ExtRoomsInterface.ts
Normal file
6
back/src/Model/Websocket/ExtRoomsInterface.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import {Rooms} from "socket.io";
|
||||||
|
|
||||||
|
export interface ExtRoomsInterface extends Rooms{
|
||||||
|
userPositionMapByRoom: any;
|
||||||
|
refreshUserPosition: any;
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
export interface Identificable {
|
|
||||||
userId: number;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import * as tg from "generic-type-guard";
|
|
||||||
|
|
||||||
export const isItemEventMessageInterface = new tg.IsInterface()
|
|
||||||
.withProperties({
|
|
||||||
itemId: tg.isNumber,
|
|
||||||
event: tg.isString,
|
|
||||||
state: tg.isUnknown,
|
|
||||||
parameters: tg.isUnknown,
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
|
20
back/src/Model/Websocket/Message.ts
Normal file
20
back/src/Model/Websocket/Message.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
export class Message {
|
||||||
|
userId: string;
|
||||||
|
roomId: string;
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
let data = JSON.parse(message);
|
||||||
|
if(!data.userId || !data.roomId){
|
||||||
|
throw Error("userId or roomId cannot be null");
|
||||||
|
}
|
||||||
|
this.userId = data.userId;
|
||||||
|
this.roomId = data.roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson() {
|
||||||
|
return {
|
||||||
|
userId: this.userId,
|
||||||
|
roomId: this.roomId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,45 @@
|
||||||
import { PointInterface } from "./PointInterface";
|
import {Message} from "./Message";
|
||||||
|
import {PointInterface} from "./PointInterface";
|
||||||
|
|
||||||
export class Point implements PointInterface {
|
export class Point implements PointInterface{
|
||||||
constructor(
|
x: number;
|
||||||
public x: number,
|
y: number;
|
||||||
public y: number,
|
direction: string;
|
||||||
public direction: string = "none",
|
|
||||||
public moving: boolean = false
|
constructor(x : number, y : number, direction : string = "none") {
|
||||||
) {}
|
if(x === null || y === null){
|
||||||
|
throw Error("position x and y cannot be null");
|
||||||
|
}
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(){
|
||||||
|
return {
|
||||||
|
x : this.x,
|
||||||
|
y: this.y,
|
||||||
|
direction: this.direction
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MessageUserPosition extends Message{
|
||||||
|
position: PointInterface;
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
let data = JSON.parse(message);
|
||||||
|
this.position = new Point(data.position.x, data.position.y, data.position.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return JSON.stringify(
|
||||||
|
Object.assign(
|
||||||
|
super.toJson(),
|
||||||
|
{
|
||||||
|
position: this.position.toJson()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,6 @@
|
||||||
import * as tg from "generic-type-guard";
|
export interface PointInterface {
|
||||||
|
x: number;
|
||||||
/*export interface PointInterface {
|
y: number;
|
||||||
readonly x: number;
|
direction: string;
|
||||||
readonly y: number;
|
toJson() : object;
|
||||||
readonly direction: string;
|
}
|
||||||
readonly moving: boolean;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
export const isPointInterface = new tg.IsInterface()
|
|
||||||
.withProperties({
|
|
||||||
x: tg.isNumber,
|
|
||||||
y: tg.isNumber,
|
|
||||||
direction: tg.isString,
|
|
||||||
moving: tg.isBoolean,
|
|
||||||
})
|
|
||||||
.get();
|
|
||||||
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
|
|
@ -1,117 +0,0 @@
|
||||||
import { PointInterface } from "./PointInterface";
|
|
||||||
import {
|
|
||||||
CharacterLayerMessage,
|
|
||||||
ItemEventMessage,
|
|
||||||
PointMessage,
|
|
||||||
PositionMessage,
|
|
||||||
} from "../../Messages/generated/messages_pb";
|
|
||||||
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
|
||||||
import Direction = PositionMessage.Direction;
|
|
||||||
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
|
||||||
import { PositionInterface } from "_Model/PositionInterface";
|
|
||||||
|
|
||||||
export class ProtobufUtils {
|
|
||||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
|
||||||
let direction: Direction;
|
|
||||||
switch (point.direction) {
|
|
||||||
case "up":
|
|
||||||
direction = Direction.UP;
|
|
||||||
break;
|
|
||||||
case "down":
|
|
||||||
direction = Direction.DOWN;
|
|
||||||
break;
|
|
||||||
case "left":
|
|
||||||
direction = Direction.LEFT;
|
|
||||||
break;
|
|
||||||
case "right":
|
|
||||||
direction = Direction.RIGHT;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("unexpected direction");
|
|
||||||
}
|
|
||||||
|
|
||||||
const position = new PositionMessage();
|
|
||||||
position.setX(point.x);
|
|
||||||
position.setY(point.y);
|
|
||||||
position.setMoving(point.moving);
|
|
||||||
position.setDirection(direction);
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toPointInterface(position: PositionMessage): PointInterface {
|
|
||||||
let direction: string;
|
|
||||||
switch (position.getDirection()) {
|
|
||||||
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
|
|
||||||
return {
|
|
||||||
x: position.getX(),
|
|
||||||
y: position.getY(),
|
|
||||||
direction,
|
|
||||||
moving: position.getMoving(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toPointMessage(point: PositionInterface): PointMessage {
|
|
||||||
const position = new PointMessage();
|
|
||||||
position.setX(Math.floor(point.x));
|
|
||||||
position.setY(Math.floor(point.y));
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toItemEvent(itemEventMessage: ItemEventMessage): ItemEventMessageInterface {
|
|
||||||
return {
|
|
||||||
itemId: itemEventMessage.getItemid(),
|
|
||||||
event: itemEventMessage.getEvent(),
|
|
||||||
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
|
||||||
state: JSON.parse(itemEventMessage.getStatejson()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
|
||||||
const itemEventMessage = new ItemEventMessage();
|
|
||||||
itemEventMessage.setItemid(itemEvent.itemId);
|
|
||||||
itemEventMessage.setEvent(itemEvent.event);
|
|
||||||
itemEventMessage.setParametersjson(JSON.stringify(itemEvent.parameters));
|
|
||||||
itemEventMessage.setStatejson(JSON.stringify(itemEvent.state));
|
|
||||||
|
|
||||||
return itemEventMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
|
||||||
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
|
||||||
const message = new CharacterLayerMessage();
|
|
||||||
message.setName(characterLayer.name);
|
|
||||||
if (characterLayer.url) {
|
|
||||||
message.setUrl(characterLayer.url);
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
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";
|
|
||||||
|
|
||||||
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 class Zone {
|
|
||||||
private things: Set<Movable> = new Set<Movable>();
|
|
||||||
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private onEnters: EntersCallback,
|
|
||||||
private onMoves: MovesCallback,
|
|
||||||
private onLeaves: LeavesCallback,
|
|
||||||
private onEmote: EmoteCallback,
|
|
||||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback,
|
|
||||||
public readonly x: number,
|
|
||||||
public readonly y: number
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user/thing leaves the zone
|
|
||||||
*/
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
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})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.notifyLeft(thing, newZone);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify listeners of this zone that this user/thing left
|
|
||||||
*/
|
|
||||||
private notifyLeft(thing: Movable, newZone: Zone | null) {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
this.onLeaves(thing, newZone, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
|
||||||
this.things.add(thing);
|
|
||||||
this.notifyEnter(thing, oldZone, position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notify listeners of this zone that this user entered
|
|
||||||
*/
|
|
||||||
private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
this.onEnters(thing, oldZone, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public move(thing: Movable, position: PositionInterface) {
|
|
||||||
if (!this.things.has(thing)) {
|
|
||||||
this.things.add(thing);
|
|
||||||
this.notifyEnter(thing, null, position);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
//if (listener !== thing) {
|
|
||||||
this.onMoves(thing, position, listener);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getThings(): Set<Movable> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
|
||||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, BatchToPusherMessage>;
|
|
||||||
export type RoomSocket = ServerWritableStream<RoomMessage, BatchToPusherRoomMessage>;
|
|
||||||
|
|
||||||
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<AdminMessage>, callback: sendUnaryData<EmptyMessage>): 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<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
|
||||||
throw new Error("Not implemented yet");
|
|
||||||
// TODO
|
|
||||||
callback(null, new EmptyMessage());
|
|
||||||
},
|
|
||||||
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): 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<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): 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<WorldFullWarningToRoomMessage>,
|
|
||||||
callback: sendUnaryData<EmptyMessage>
|
|
||||||
): 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<RefreshRoomPromptMessage>,
|
|
||||||
callback: sendUnaryData<EmptyMessage>
|
|
||||||
): 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 };
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { App as _App, AppOptions } from "uWebSockets.js";
|
|
||||||
import BaseApp from "./baseapp";
|
|
||||||
import { extend } from "./utils";
|
|
||||||
import { UwsApp } from "./types";
|
|
||||||
|
|
||||||
class App extends (<UwsApp>_App) {
|
|
||||||
constructor(options: AppOptions = {}) {
|
|
||||||
super(options); // eslint-disable-line constructor-super
|
|
||||||
extend(this, new BaseApp());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,111 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
class BaseApp {
|
|
||||||
_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)");
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
|
@ -1,101 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
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;
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
console.log("Enter form data");
|
|
||||||
options.headers = {
|
|
||||||
"content-type": contType,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const busb = new Busboy(options);
|
|
||||||
const ret = {};
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
if (fieldname.endsWith("[]")) {
|
|
||||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
|
||||||
if (Array.isArray(ret[fieldname])) {
|
|
||||||
ret[fieldname].push(value);
|
|
||||||
} else {
|
|
||||||
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;
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
|
|
||||||
import BaseApp from "./baseapp";
|
|
||||||
import { extend } from "./utils";
|
|
||||||
import { UwsApp } from "./types";
|
|
||||||
|
|
||||||
class SSLApp extends (<UwsApp>_SSLApp) {
|
|
||||||
constructor(options: AppOptions) {
|
|
||||||
super(options); // eslint-disable-line constructor-super
|
|
||||||
extend(this, new BaseApp());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SSLApp;
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
|
||||||
|
|
||||||
export type UwsApp = {
|
|
||||||
(options: AppOptions): TemplatedApp;
|
|
||||||
new (options: AppOptions): TemplatedApp;
|
|
||||||
prototype: TemplatedApp;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
|
||||||
|
|
||||||
export {};
|
|
|
@ -1,38 +0,0 @@
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
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 stob(stream: ReadStream): Promise<Buffer> {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { extend, stob };
|
|
|
@ -1,19 +0,0 @@
|
||||||
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());
|
|
||||||
};
|
|
||||||
|
|
||||||
export { App, SSLApp, getQuery };
|
|
||||||
export * from "./server/types";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
App,
|
|
||||||
SSLApp,
|
|
||||||
getQuery,
|
|
||||||
...types,
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
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";
|
|
||||||
|
|
||||||
class AdminApi {
|
|
||||||
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
|
||||||
if (!ADMIN_API_URL) {
|
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: { playUri: string } = {
|
|
||||||
playUri,
|
|
||||||
};
|
|
||||||
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const adminApi = new AdminApi();
|
|
|
@ -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<typeof isCharacterTexture>;
|
|
|
@ -1,21 +0,0 @@
|
||||||
import * as tg from "generic-type-guard";
|
|
||||||
import { isCharacterTexture } from "./CharacterTexture";
|
|
||||||
import { isAny, isNumber } from "generic-type-guard";
|
|
||||||
|
|
||||||
/*const isNumericEnum =
|
|
||||||
<T extends { [n: number]: string }>(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<typeof isMapDetailsData>;
|
|
|
@ -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<typeof isRoomRedirect>;
|
|
|
@ -1,3 +0,0 @@
|
||||||
export const arrayIntersect = (array1: string[], array2: string[]): boolean => {
|
|
||||||
return array1.filter((value) => array2.includes(value)).length > 0;
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { EventEmitter } from "events";
|
|
||||||
|
|
||||||
const clientJoinEvent = "clientJoin";
|
|
||||||
const clientLeaveEvent = "clientLeave";
|
|
||||||
|
|
||||||
class ClientEventsEmitter extends EventEmitter {
|
|
||||||
emitClientJoin(clientUUid: string, roomId: string): void {
|
|
||||||
this.emit(clientJoinEvent, clientUUid, roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitClientLeave(clientUUid: string, roomId: string): void {
|
|
||||||
this.emit(clientLeaveEvent, clientUUid, roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void {
|
|
||||||
this.on(clientJoinEvent, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerToClientLeave(callback: (clientUUid: string, roomId: string) => void): void {
|
|
||||||
this.on(clientLeaveEvent, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregisterFromClientJoin(callback: (clientUUid: string, roomId: string) => void): void {
|
|
||||||
this.removeListener(clientJoinEvent, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregisterFromClientLeave(callback: (clientUUid: string, roomId: string) => void): void {
|
|
||||||
this.removeListener(clientLeaveEvent, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clientEventsEmitter = new ClientEventsEmitter();
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
|
|
||||||
|
|
||||||
function secNSec2ms(secNSec: Array<number> | number) {
|
|
||||||
if (Array.isArray(secNSec)) {
|
|
||||||
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
|
||||||
}
|
|
||||||
return secNSec / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CpuTracker {
|
|
||||||
private cpuPercent: number = 0;
|
|
||||||
private overHeating: boolean = false;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
let time = process.hrtime.bigint();
|
|
||||||
let usage = process.cpuUsage();
|
|
||||||
setInterval(() => {
|
|
||||||
const elapTime = process.hrtime.bigint();
|
|
||||||
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);
|
|
||||||
|
|
||||||
time = elapTime;
|
|
||||||
|
|
||||||
if (!this.overHeating && this.cpuPercent > CPU_OVERHEAT_THRESHOLD) {
|
|
||||||
this.overHeating = true;
|
|
||||||
console.warn('CPU high threshold alert. Going in "overheat" mode');
|
|
||||||
} else if (this.overHeating && this.cpuPercent <= CPU_OVERHEAT_THRESHOLD) {
|
|
||||||
this.overHeating = false;
|
|
||||||
console.log('CPU is back to normal. Canceling "overheat" mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
/*console.log('elapsed time ms: ', elapTimeMS)
|
|
||||||
console.log('elapsed user ms: ', elapUserMS)
|
|
||||||
console.log('elapsed system ms:', elapSystMS)
|
|
||||||
console.log('cpu percent: ', this.cpuPercent)*/
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCpuPercent(): number {
|
|
||||||
return this.cpuPercent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isOverHeating(): boolean {
|
|
||||||
return this.overHeating;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cpuTracker = new CpuTracker();
|
|
||||||
|
|
||||||
export { cpuTracker };
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { Counter, Gauge } from "prom-client";
|
|
||||||
|
|
||||||
//this class should manage all the custom metrics used by prometheus
|
|
||||||
class GaugeManager {
|
|
||||||
private nbClientsGauge: Gauge<string>;
|
|
||||||
private nbClientsPerRoomGauge: Gauge<string>;
|
|
||||||
private nbGroupsPerRoomGauge: Gauge<string>;
|
|
||||||
private nbGroupsPerRoomCounter: Counter<string>;
|
|
||||||
private nbRoomsGauge: Gauge<string>;
|
|
||||||
|
|
||||||
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: [],
|
|
||||||
});
|
|
||||||
this.nbClientsPerRoomGauge = new Gauge({
|
|
||||||
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"],
|
|
||||||
});
|
|
||||||
this.nbGroupsPerRoomGauge = new Gauge({
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
decNbClientPerRoomGauge(roomId: string): void {
|
|
||||||
this.nbClientsGauge.dec();
|
|
||||||
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const gaugeManager = new GaugeManager();
|
|
|
@ -1 +0,0 @@
|
||||||
export class LocalUrlError extends Error {}
|
|
|
@ -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<ITiledMap> {
|
|
||||||
// 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<boolean> {
|
|
||||||
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();
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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 };
|
|
|
@ -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<number>>;
|
|
||||||
private readonly hdel: OmitThisParameter<(arg1: string, arg2: string) => Promise<number>>;
|
|
||||||
|
|
||||||
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<number> {
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 };
|
|
|
@ -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<number>;
|
|
||||||
}
|
|
|
@ -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<number> {
|
|
||||||
return Promise.resolve(0);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,874 +0,0 @@
|
||||||
import { GameRoom } from "../Model/GameRoom";
|
|
||||||
import {
|
|
||||||
ItemEventMessage,
|
|
||||||
ItemStateMessage,
|
|
||||||
PlayGlobalMessage,
|
|
||||||
PointMessage,
|
|
||||||
RoomJoinedMessage,
|
|
||||||
ServerToClientMessage,
|
|
||||||
SilentMessage,
|
|
||||||
SubMessage,
|
|
||||||
UserMovedMessage,
|
|
||||||
UserMovesMessage,
|
|
||||||
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,
|
|
||||||
} 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 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";
|
|
||||||
|
|
||||||
const debug = Debug("sockermanager");
|
|
||||||
|
|
||||||
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 class SocketManager {
|
|
||||||
//private rooms = new Map<string, GameRoom>();
|
|
||||||
// List of rooms in process of loading.
|
|
||||||
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
|
||||||
gaugeManager.incNbClientPerRoomGauge(roomId);
|
|
||||||
});
|
|
||||||
clientEventsEmitter.registerToClientLeave((clientUUid: string, roomId: string) => {
|
|
||||||
gaugeManager.decNbClientPerRoomGauge(roomId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
|
||||||
const userMoves = userMovesMessage.toObject();
|
|
||||||
const position = userMovesMessage.getPosition();
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
|
||||||
|
|
||||||
const subMessage = new SubMessage();
|
|
||||||
subMessage.setItemeventmessage(itemEventMessage);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
room.setItemState(itemEvent.itemId, itemEvent.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage): Promise<void> {
|
|
||||||
return room.setVariable(variableMessage.getName(), variableMessage.getValue(), user);
|
|
||||||
}
|
|
||||||
|
|
||||||
emitVideo(room: GameRoom, user: User, 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."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
|
|
||||||
webrtcSignalToClient.setUserid(user.id);
|
|
||||||
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);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
emitScreenSharing(room: GameRoom, user: User, 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."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
|
|
||||||
webrtcSignalToClient.setUserid(user.id);
|
|
||||||
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);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
leaveRoom(room: GameRoom, user: User) {
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
|
|
||||||
console.log("A user left");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
|
||||||
//check and create new room
|
|
||||||
let roomPromise = this.roomsPromises.get(roomId);
|
|
||||||
if (roomPromise === undefined) {
|
|
||||||
roomPromise = GameRoom.create(
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
return roomPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async joinRoom(
|
|
||||||
socket: UserSocket,
|
|
||||||
joinRoomMessage: JoinRoomMessage
|
|
||||||
): Promise<{ room: GameRoom; user: User }> {
|
|
||||||
const roomId = joinRoomMessage.getRoomid();
|
|
||||||
|
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
|
||||||
|
|
||||||
//join world
|
|
||||||
const user = room.join(socket, joinRoomMessage);
|
|
||||||
|
|
||||||
clientEventsEmitter.emitClientJoin(user.uuid, roomId);
|
|
||||||
console.log(new Date().toISOString() + " A user joined");
|
|
||||||
return { room, user };
|
|
||||||
}
|
|
||||||
|
|
||||||
private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) {
|
|
||||||
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 subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setUserjoinedzonemessage(userJoinedZoneMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, listener);
|
|
||||||
//listener.emitInBatch(subMessage);
|
|
||||||
} else if (thing instanceof Group) {
|
|
||||||
this.emitCreateUpdateGroupEvent(listener, fromZone, thing);
|
|
||||||
} else {
|
|
||||||
console.error("Unexpected type for Movable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void {
|
|
||||||
if (thing instanceof User) {
|
|
||||||
const userMovedMessage = new UserMovedMessage();
|
|
||||||
userMovedMessage.setUserid(thing.id);
|
|
||||||
userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setUsermovedmessage(userMovedMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, listener);
|
|
||||||
//listener.emitInBatch(subMessage);
|
|
||||||
//console.log("Sending USER_MOVED event");
|
|
||||||
} else if (thing instanceof Group) {
|
|
||||||
this.emitCreateUpdateGroupEvent(listener, null, thing);
|
|
||||||
} else {
|
|
||||||
console.error("Unexpected type for Movable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) {
|
|
||||||
if (thing instanceof User) {
|
|
||||||
this.emitUserLeftEvent(listener, thing.id, newZone);
|
|
||||||
} else if (thing instanceof Group) {
|
|
||||||
this.emitDeleteGroupEvent(listener, thing.getId(), newZone);
|
|
||||||
} else {
|
|
||||||
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 {
|
|
||||||
const position = group.getPosition();
|
|
||||||
const pointMessage = new PointMessage();
|
|
||||||
pointMessage.setX(Math.floor(position.x));
|
|
||||||
pointMessage.setY(Math.floor(position.y));
|
|
||||||
const groupUpdateMessage = new GroupUpdateZoneMessage();
|
|
||||||
groupUpdateMessage.setGroupid(group.getId());
|
|
||||||
groupUpdateMessage.setPosition(pointMessage);
|
|
||||||
groupUpdateMessage.setGroupsize(group.getSize);
|
|
||||||
groupUpdateMessage.setFromzone(this.toProtoZone(fromZone));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setGroupupdatezonemessage(groupUpdateMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, client);
|
|
||||||
//client.emitInBatch(subMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void {
|
|
||||||
const groupDeleteMessage = new GroupLeftZoneMessage();
|
|
||||||
groupDeleteMessage.setGroupid(groupId);
|
|
||||||
groupDeleteMessage.setTozone(this.toProtoZone(newZone));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setGroupleftzonemessage(groupDeleteMessage);
|
|
||||||
|
|
||||||
emitZoneMessage(subMessage, client);
|
|
||||||
//user.emitInBatch(subMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void {
|
|
||||||
const userLeftMessage = new UserLeftZoneMessage();
|
|
||||||
userLeftMessage.setUserid(userId);
|
|
||||||
userLeftMessage.setTozone(this.toProtoZone(newZone));
|
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
|
||||||
subMessage.setUserleftzonemessage(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private joinWebRtcRoom(user: User, group: Group) {
|
|
||||||
for (const otherUser of group.getUsers()) {
|
|
||||||
if (user === otherUser) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.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);
|
|
||||||
|
|
||||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
|
||||||
webrtcStartMessage2.setUserid(user.id);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
private disConnectedUser(user: User, group: Group) {
|
|
||||||
// Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection
|
|
||||||
// which will be shut for the other player).
|
|
||||||
// However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player,
|
|
||||||
// the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing).
|
|
||||||
// So we also send the disconnect event to the other player.
|
|
||||||
for (const otherUser of group.getUsers()) {
|
|
||||||
if (user === otherUser) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const webrtcDisconnectMessage1 = new WebRtcDisconnectMessage();
|
|
||||||
webrtcDisconnectMessage1.setUserid(user.id);
|
|
||||||
|
|
||||||
const serverToClientMessage1 = new ServerToClientMessage();
|
|
||||||
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
|
|
||||||
|
|
||||||
//if (!otherUser.socket.disconnecting) {
|
|
||||||
otherUser.socket.write(serverToClientMessage1);
|
|
||||||
//}
|
|
||||||
|
|
||||||
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
|
|
||||||
webrtcDisconnectMessage2.setUserid(otherUser.id);
|
|
||||||
|
|
||||||
const serverToClientMessage2 = new ServerToClientMessage();
|
|
||||||
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
|
|
||||||
|
|
||||||
//if (!user.socket.disconnecting) {
|
|
||||||
user.socket.write(serverToClientMessage2);
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getWorlds(): Map<string, PromiseLike<GameRoom>> {
|
|
||||||
return this.roomsPromises;
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleQueryJitsiJwtMessage(user: User, 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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's see if the current client has
|
|
||||||
const isAdmin = user.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 sendJitsiJwtMessage = new SendJitsiJwtMessage();
|
|
||||||
sendJitsiJwtMessage.setJitsiroom(room);
|
|
||||||
sendJitsiJwtMessage.setJwt(jwt);
|
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
|
||||||
serverToClientMessage.setSendjitsijwtmessage(sendJitsiJwtMessage);
|
|
||||||
|
|
||||||
user.socket.write(serverToClientMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
|
|
||||||
const sendUserMessage = new SendUserMessage();
|
|
||||||
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
|
|
||||||
sendUserMessage.setType(sendUserMessageToSend.getType());
|
|
||||||
|
|
||||||
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<void> {
|
|
||||||
const room = await this.roomsPromises.get(roomId);
|
|
||||||
if (!room) {
|
|
||||||
throw new Error("In addZoneListener, could not find room with id '" + roomId + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
call.write(batchMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
|
||||||
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<GameRoom> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const socketManager = new SocketManager();
|
|
|
@ -1,9 +0,0 @@
|
||||||
/**
|
|
||||||
* Errors related to variable handling.
|
|
||||||
*/
|
|
||||||
export class VariableError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
Object.setPrototypeOf(this, VariableError.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<string, string>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of variables that are allowed
|
|
||||||
*/
|
|
||||||
private variableObjects: Map<string, Variable> | 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<VariablesManager> {
|
|
||||||
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<string, Variable> {
|
|
||||||
const objects = new Map<string, Variable>();
|
|
||||||
for (const layer of map.layers) {
|
|
||||||
this.recursiveFindVariablesInLayer(layer, objects);
|
|
||||||
}
|
|
||||||
return objects;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map<string, Variable>): 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<string, string> {
|
|
||||||
if (this.variableObjects === undefined) {
|
|
||||||
return this._variables;
|
|
||||||
}
|
|
||||||
|
|
||||||
const readableVariables = new Map<string, string>();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import {arrayIntersect} from "../src/Services/ArrayHelper";
|
|
||||||
|
|
||||||
|
|
||||||
describe("RoomIdentifier", () => {
|
|
||||||
it("should return true on intersect", () => {
|
|
||||||
expect(arrayIntersect(['admin', 'user'], ['admin', 'superAdmin'])).toBe(true);
|
|
||||||
});
|
|
||||||
it("should be reflexive", () => {
|
|
||||||
expect(arrayIntersect(['admin', 'superAdmin'], ['admin', 'user'])).toBe(true);
|
|
||||||
});
|
|
||||||
it("should return false on non intersect", () => {
|
|
||||||
expect(arrayIntersect(['admin', 'user'], ['superAdmin'])).toBe(false);
|
|
||||||
});
|
|
||||||
})
|
|
|
@ -1,148 +0,0 @@
|
||||||
import "jasmine";
|
|
||||||
import { ConnectCallback, DisconnectCallback, GameRoom } 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";
|
|
||||||
|
|
||||||
function createMockUser(userId: number): User {
|
|
||||||
return {
|
|
||||||
userId,
|
|
||||||
} as unknown as User;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
let connectCalledNumber: number = 0;
|
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
|
||||||
connectCalledNumber++;
|
|
||||||
};
|
|
||||||
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 user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100));
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(261, 100));
|
|
||||||
|
|
||||||
expect(connectCalledNumber).toBe(0);
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(101, 100));
|
|
||||||
|
|
||||||
expect(connectCalledNumber).toBe(2);
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(102, 100));
|
|
||||||
expect(connectCalledNumber).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should connect 3 users", async () => {
|
|
||||||
let connectCalled: boolean = false;
|
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
|
||||||
connectCalled = true;
|
|
||||||
};
|
|
||||||
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 user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 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));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(false);
|
|
||||||
|
|
||||||
world.updatePosition(user3, new Point(309, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should disconnect user1 and user2", async () => {
|
|
||||||
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 user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
|
||||||
|
|
||||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
|
||||||
expect(disconnectCallNumber).toBe(0);
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100));
|
|
||||||
|
|
||||||
expect(disconnectCallNumber).toBe(2);
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(262, 100));
|
|
||||||
expect(disconnectCallNumber).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
31
back/tests/MessageTest.ts
Normal file
31
back/tests/MessageTest.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import "jasmine";
|
||||||
|
import {Message} from "../src/Model/Websocket/Message";
|
||||||
|
|
||||||
|
describe("Message Model", () => {
|
||||||
|
it("should find userId and roomId", () => {
|
||||||
|
let message = JSON.stringify({userId: "test1", roomId: "test2"});
|
||||||
|
let messageObject = new Message(message);
|
||||||
|
expect(messageObject.userId).toBe("test1");
|
||||||
|
expect(messageObject.roomId).toBe("test2");
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should expose a toJson method", () => {
|
||||||
|
let message = JSON.stringify({userId: "test1", roomId: "test2"});
|
||||||
|
let messageObject = new Message(message);
|
||||||
|
expect(messageObject.toJson()).toEqual({userId: "test1", roomId: "test2"});
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should find throw error when no userId", () => {
|
||||||
|
let message = JSON.stringify({roomId: "test2"});
|
||||||
|
expect(() => {
|
||||||
|
let messageObject = new Message(message);
|
||||||
|
}).toThrow(new Error("userId or roomId cannot be null"));
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should find throw error when no roomId", () => {
|
||||||
|
let message = JSON.stringify({userId: "test1"});
|
||||||
|
expect(() => {
|
||||||
|
let messageObject = new Message(message);
|
||||||
|
}).toThrow(new Error("userId or roomId cannot be null"));
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,246 +0,0 @@
|
||||||
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";
|
|
||||||
|
|
||||||
describe("PositionNotifier", () => {
|
|
||||||
it("should receive notifications when player moves", () => {
|
|
||||||
let enterTriggered = false;
|
|
||||||
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 user1 = new User(
|
|
||||||
1,
|
|
||||||
"test",
|
|
||||||
"10.0.0.2",
|
|
||||||
{
|
|
||||||
x: 500,
|
|
||||||
y: 500,
|
|
||||||
moving: false,
|
|
||||||
direction: "down",
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
positionNotifier,
|
|
||||||
{} as UserSocket,
|
|
||||||
[],
|
|
||||||
null,
|
|
||||||
"foo",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const user2 = new User(
|
|
||||||
2,
|
|
||||||
"test",
|
|
||||||
"10.0.0.2",
|
|
||||||
{
|
|
||||||
x: -9999,
|
|
||||||
y: -9999,
|
|
||||||
moving: false,
|
|
||||||
direction: "down",
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
positionNotifier,
|
|
||||||
{} as UserSocket,
|
|
||||||
[],
|
|
||||||
null,
|
|
||||||
"foo",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
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, {
|
|
||||||
left: 200,
|
|
||||||
right: 600,
|
|
||||||
top: 100,
|
|
||||||
bottom: 500
|
|
||||||
});*/
|
|
||||||
|
|
||||||
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 });
|
|
||||||
|
|
||||||
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 });
|
|
||||||
|
|
||||||
expect(enterTriggered).toBe(false);
|
|
||||||
expect(moveTriggered).toBe(false);
|
|
||||||
expect(leaveTriggered).toBe(true);
|
|
||||||
leaveTriggered = false;
|
|
||||||
|
|
||||||
// Move back in
|
|
||||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
|
||||||
expect(enterTriggered).toBe(true);
|
|
||||||
expect(moveTriggered).toBe(false);
|
|
||||||
expect(leaveTriggered).toBe(false);
|
|
||||||
enterTriggered = false;
|
|
||||||
|
|
||||||
// Leave the room
|
|
||||||
positionNotifier.leave(user2);
|
|
||||||
//positionNotifier.removeViewport(user2);
|
|
||||||
expect(enterTriggered).toBe(false);
|
|
||||||
expect(moveTriggered).toBe(false);
|
|
||||||
expect(leaveTriggered).toBe(true);
|
|
||||||
leaveTriggered = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should receive notifications when camera moves", () => {
|
|
||||||
let enterTriggered = false;
|
|
||||||
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 user1 = new User(
|
|
||||||
1,
|
|
||||||
"test",
|
|
||||||
"10.0.0.2",
|
|
||||||
{
|
|
||||||
x: 500,
|
|
||||||
y: 500,
|
|
||||||
moving: false,
|
|
||||||
direction: "down",
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
positionNotifier,
|
|
||||||
{} as UserSocket,
|
|
||||||
[],
|
|
||||||
null,
|
|
||||||
"foo",
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
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 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, {
|
|
||||||
left: 200,
|
|
||||||
right: 600,
|
|
||||||
top: 100,
|
|
||||||
bottom: 500
|
|
||||||
});*/
|
|
||||||
positionNotifier.enter(user1);
|
|
||||||
positionNotifier.enter(user2);
|
|
||||||
|
|
||||||
//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 });
|
|
||||||
|
|
||||||
expect(enterTriggered).toBe(true);
|
|
||||||
expect(moveTriggered).toBe(false);
|
|
||||||
expect(leaveTriggered).toBe(true);
|
|
||||||
enterTriggered = false;
|
|
||||||
leaveTriggered = false;
|
|
||||||
|
|
||||||
// Add a listener, but the user in not in this zone.
|
|
||||||
positionNotifier.addZoneListener(listener, 10, 10);
|
|
||||||
|
|
||||||
/*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, {
|
|
||||||
left: 901,
|
|
||||||
right: 1001,
|
|
||||||
top: 100,
|
|
||||||
bottom: 500
|
|
||||||
});*/
|
|
||||||
|
|
||||||
expect(enterTriggered).toBe(false);
|
|
||||||
expect(moveTriggered).toBe(false);
|
|
||||||
expect(leaveTriggered).toBe(false);
|
|
||||||
|
|
||||||
// Move the viewport back on the user.
|
|
||||||
positionNotifier.addZoneListener(listener, 1, 1);
|
|
||||||
/*newUsers = positionNotifier.setViewport(user1, {
|
|
||||||
left: 200,
|
|
||||||
right: 600,
|
|
||||||
top: 100,
|
|
||||||
bottom: 500
|
|
||||||
});*/
|
|
||||||
|
|
||||||
expect(enterTriggered).toBe(false);
|
|
||||||
expect(moveTriggered).toBe(false);
|
|
||||||
expect(leaveTriggered).toBe(false);
|
|
||||||
enterTriggered = false;
|
|
||||||
//expect(newUsers.length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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},
|
|
||||||
])
|
|
||||||
});*/
|
|
||||||
});
|
|
|
@ -3,16 +3,15 @@
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "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'. */
|
"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. */
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
"sourceMap": true, /* Generates corresponding '.map' file. */
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
@ -31,14 +30,14 @@
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
"noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */ // Disabled because of sifrr server that is monkey patching HttpResponse
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
/* Additional Checks */
|
/* Additional Checks */
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
@ -67,6 +66,7 @@
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
/* Advanced Options */
|
/* Advanced Options */
|
||||||
|
"downlevelIteration": true,
|
||||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2872
back/yarn.lock
2872
back/yarn.lock
File diff suppressed because it is too large
Load diff
3
benchmark/.gitignore
vendored
3
benchmark/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
/node_modules/
|
|
||||||
/artillery_output.html
|
|
||||||
/artillery_output.json
|
|
|
@ -1,69 +0,0 @@
|
||||||
# Load testing
|
|
||||||
|
|
||||||
Load testing is performed with Artillery.
|
|
||||||
|
|
||||||
Install:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd benchmark
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Running the tests (on one core):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd benchmark
|
|
||||||
npm run start
|
|
||||||
```
|
|
||||||
|
|
||||||
You can adapt the `socketio-load-test.yaml` file to increase/decrease load.
|
|
||||||
|
|
||||||
Default settings are:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
phases:
|
|
||||||
- duration: 20
|
|
||||||
arrivalRate: 2
|
|
||||||
```
|
|
||||||
|
|
||||||
which means: during 20 seconds, 2 users will be added every second (peaking at 40 simultaneous users).
|
|
||||||
|
|
||||||
Important: don't go above 40 simultaneous users for Artillery, otherwise, it is Artillery that will fail to run the tests properly.
|
|
||||||
To know, simply run "top". The "node" process for Artillery should never reach 100%.
|
|
||||||
|
|
||||||
Reports are generated in `artillery_output.html`.
|
|
||||||
|
|
||||||
# Multicore tests
|
|
||||||
|
|
||||||
You will want to test with Artillery running on multiple cores.
|
|
||||||
|
|
||||||
You can use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./artillery_multi_core.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This will trigger 4 Artillery instances in parallel.
|
|
||||||
|
|
||||||
Beware, the report generated is generated for only one instance.
|
|
||||||
|
|
||||||
# How to test, what to track?
|
|
||||||
|
|
||||||
While testing, you can check:
|
|
||||||
|
|
||||||
- CPU load of WorkAdventure API node process (it should not reach 100%)
|
|
||||||
- Get metrics at the end of the run: `http://api.workadventure.localhost/metrics`
|
|
||||||
In particular, look for:
|
|
||||||
```
|
|
||||||
# HELP nodejs_eventloop_lag_max_seconds The maximum recorded event loop delay.
|
|
||||||
# TYPE nodejs_eventloop_lag_max_seconds gauge
|
|
||||||
nodejs_eventloop_lag_max_seconds 23.991418879
|
|
||||||
```
|
|
||||||
This is the maximum time it took Node to process an event (you need to restart node after each test to reset this counter)
|
|
||||||
- Generate a profiling using "node --prof" by switching the command in docker-compose.yaml:
|
|
||||||
```
|
|
||||||
#command: yarn dev
|
|
||||||
command: yarn run profile
|
|
||||||
```
|
|
||||||
Read https://nodejs.org/en/docs/guides/simple-profiling/ on how to generate a profile.
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
yarn run start &
|
|
||||||
pid1=$!
|
|
||||||
yarn run start &
|
|
||||||
pid2=$!
|
|
||||||
yarn run start &
|
|
||||||
pid3=$!
|
|
||||||
yarn run start &
|
|
||||||
pid4=$!
|
|
||||||
|
|
||||||
wait $pid1
|
|
||||||
wait $pid2
|
|
||||||
wait $pid3
|
|
||||||
wait $pid4
|
|
|
@ -1,71 +0,0 @@
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomConnection.setWebsocketFactory((url: string) => {
|
|
||||||
return new WebSocket(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function startOneUser(): Promise<void> {
|
|
||||||
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
|
||||||
{
|
|
||||||
x: 783,
|
|
||||||
y: 170
|
|
||||||
}, {
|
|
||||||
top: 0,
|
|
||||||
bottom: 200,
|
|
||||||
left: 500,
|
|
||||||
right: 800
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
const connection = onConnect.connection;
|
|
||||||
|
|
||||||
connection.onUserMoved(() => {
|
|
||||||
userMovedCount++;
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(connection.getUserId());
|
|
||||||
|
|
||||||
let angle = Math.random() * Math.PI * 2;
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
const x = Math.floor(320 + 1472/2 * (1 + Math.sin(angle)));
|
|
||||||
const y = Math.floor(200 + 1090/2 * (1 + Math.cos(angle)));
|
|
||||||
|
|
||||||
connection.sharePosition(x, y, 'down', true, {
|
|
||||||
top: y - 200,
|
|
||||||
bottom: y + 200,
|
|
||||||
left: x - 320,
|
|
||||||
right: x + 320
|
|
||||||
})
|
|
||||||
|
|
||||||
angle += 0.05;
|
|
||||||
|
|
||||||
await sleep(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(10000);
|
|
||||||
connection.closeConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
connectionManager.initBenchmark();
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
for (let userNo = 0; userNo < 160; userNo++) {
|
|
||||||
const promise = startOneUser();
|
|
||||||
promises.push(promise);
|
|
||||||
// Wait 0.5s between adding users
|
|
||||||
await sleep(125);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
console.log('User moved count: '+userMovedCount);
|
|
||||||
})();
|
|
706
benchmark/package-lock.json
generated
706
benchmark/package-lock.json
generated
|
@ -1,706 +0,0 @@
|
||||||
{
|
|
||||||
"name": "workadventure-artillery",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"requires": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": {
|
|
||||||
"version": "14.6.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.4.tgz",
|
|
||||||
"integrity": "sha512-Wk7nG1JSaMfMpoMJDKUsWYugliB2Vy55pdjLpmLixeyMi7HizW2I/9QoxsPCkXl3dO+ZOVqPumKaDUv5zJu2uQ=="
|
|
||||||
},
|
|
||||||
"@types/strip-bom": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I="
|
|
||||||
},
|
|
||||||
"@types/strip-json-comments": {
|
|
||||||
"version": "0.0.30",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz",
|
|
||||||
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ=="
|
|
||||||
},
|
|
||||||
"@types/ws": {
|
|
||||||
"version": "7.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.7.tgz",
|
|
||||||
"integrity": "sha512-UUFC/xxqFLP17hTva8/lVT0SybLUrfSD9c+iapKb0fEiC8uoDbA+xuZ3pAN603eW+bY8ebSMLm9jXdIPnD0ZgA==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"anymatch": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
|
|
||||||
"requires": {
|
|
||||||
"normalize-path": "^3.0.0",
|
|
||||||
"picomatch": "^2.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"arg": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
|
||||||
},
|
|
||||||
"array-find-index": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E="
|
|
||||||
},
|
|
||||||
"balanced-match": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
|
||||||
},
|
|
||||||
"binary-extensions": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ=="
|
|
||||||
},
|
|
||||||
"brace-expansion": {
|
|
||||||
"version": "1.1.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
|
||||||
"requires": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"braces": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
|
||||||
"requires": {
|
|
||||||
"fill-range": "^7.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"buffer-from": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
|
||||||
},
|
|
||||||
"camelcase-keys": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
|
||||||
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
|
|
||||||
"requires": {
|
|
||||||
"camelcase": "^2.0.0",
|
|
||||||
"map-obj": "^1.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"camelcase": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
|
|
||||||
"integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chokidar": {
|
|
||||||
"version": "3.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz",
|
|
||||||
"integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==",
|
|
||||||
"requires": {
|
|
||||||
"anymatch": "~3.1.1",
|
|
||||||
"braces": "~3.0.2",
|
|
||||||
"fsevents": "~2.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.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"concat-map": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
|
||||||
},
|
|
||||||
"currently-unhandled": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
|
||||||
"integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
|
|
||||||
"requires": {
|
|
||||||
"array-find-index": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dateformat": {
|
|
||||||
"version": "1.0.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
|
|
||||||
"integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
|
|
||||||
"requires": {
|
|
||||||
"get-stdin": "^4.0.1",
|
|
||||||
"meow": "^3.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"decamelize": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
|
||||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
|
||||||
},
|
|
||||||
"diff": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
|
|
||||||
},
|
|
||||||
"dynamic-dedupe": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz",
|
|
||||||
"integrity": "sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE=",
|
|
||||||
"requires": {
|
|
||||||
"xtend": "^4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error-ex": {
|
|
||||||
"version": "1.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
|
||||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
|
||||||
"requires": {
|
|
||||||
"is-arrayish": "^0.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fill-range": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
|
||||||
"requires": {
|
|
||||||
"to-regex-range": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"find-up": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
|
|
||||||
"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
|
|
||||||
"requires": {
|
|
||||||
"path-exists": "^2.0.0",
|
|
||||||
"pinkie-promise": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fs.realpath": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
|
||||||
},
|
|
||||||
"fsevents": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"function-bind": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
|
||||||
},
|
|
||||||
"get-stdin": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
|
|
||||||
"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
|
|
||||||
},
|
|
||||||
"glob": {
|
|
||||||
"version": "7.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
|
||||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
|
||||||
"requires": {
|
|
||||||
"fs.realpath": "^1.0.0",
|
|
||||||
"inflight": "^1.0.4",
|
|
||||||
"inherits": "2",
|
|
||||||
"minimatch": "^3.0.4",
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"path-is-absolute": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"glob-parent": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
|
||||||
"requires": {
|
|
||||||
"is-glob": "^4.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"graceful-fs": {
|
|
||||||
"version": "4.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
|
|
||||||
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
|
|
||||||
},
|
|
||||||
"has": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
|
||||||
"requires": {
|
|
||||||
"function-bind": "^1.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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=="
|
|
||||||
},
|
|
||||||
"indent-string": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
|
|
||||||
"integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
|
|
||||||
"requires": {
|
|
||||||
"repeating": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"inflight": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
|
||||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
|
||||||
"requires": {
|
|
||||||
"once": "^1.3.0",
|
|
||||||
"wrappy": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"inherits": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
|
||||||
},
|
|
||||||
"is-arrayish": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
|
||||||
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
|
|
||||||
},
|
|
||||||
"is-binary-path": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
|
||||||
"requires": {
|
|
||||||
"binary-extensions": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is-core-module": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw==",
|
|
||||||
"requires": {
|
|
||||||
"has": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is-extglob": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
|
||||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
|
|
||||||
},
|
|
||||||
"is-finite": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w=="
|
|
||||||
},
|
|
||||||
"is-glob": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
|
||||||
"requires": {
|
|
||||||
"is-extglob": "^2.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is-number": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
|
||||||
},
|
|
||||||
"is-utf8": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
|
|
||||||
"integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
|
|
||||||
},
|
|
||||||
"load-json-file": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
|
||||||
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
|
|
||||||
"requires": {
|
|
||||||
"graceful-fs": "^4.1.2",
|
|
||||||
"parse-json": "^2.2.0",
|
|
||||||
"pify": "^2.0.0",
|
|
||||||
"pinkie-promise": "^2.0.0",
|
|
||||||
"strip-bom": "^2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"pify": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
|
||||||
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"loud-rejection": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
|
||||||
"integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
|
|
||||||
"requires": {
|
|
||||||
"currently-unhandled": "^0.4.1",
|
|
||||||
"signal-exit": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"make-error": {
|
|
||||||
"version": "1.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
|
||||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
|
||||||
},
|
|
||||||
"map-obj": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
|
|
||||||
},
|
|
||||||
"meow": {
|
|
||||||
"version": "3.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
|
|
||||||
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
|
|
||||||
"requires": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minimatch": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
|
||||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
|
||||||
"requires": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"minimist": {
|
|
||||||
"version": "1.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
|
||||||
},
|
|
||||||
"mkdirp": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
|
||||||
},
|
|
||||||
"normalize-package-data": {
|
|
||||||
"version": "2.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
|
||||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
|
||||||
"requires": {
|
|
||||||
"hosted-git-info": "^2.1.4",
|
|
||||||
"resolve": "^1.10.0",
|
|
||||||
"semver": "2 || 3 || 4 || 5",
|
|
||||||
"validate-npm-package-license": "^3.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"normalize-path": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
|
|
||||||
},
|
|
||||||
"object-assign": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
|
||||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
|
||||||
},
|
|
||||||
"once": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
|
||||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
|
||||||
"requires": {
|
|
||||||
"wrappy": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"parse-json": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
|
|
||||||
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
|
|
||||||
"requires": {
|
|
||||||
"error-ex": "^1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"path-exists": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
|
|
||||||
"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
|
|
||||||
"requires": {
|
|
||||||
"pinkie-promise": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"path-is-absolute": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
|
||||||
},
|
|
||||||
"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=="
|
|
||||||
},
|
|
||||||
"path-type": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
|
|
||||||
"integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
|
|
||||||
"requires": {
|
|
||||||
"graceful-fs": "^4.1.2",
|
|
||||||
"pify": "^2.0.0",
|
|
||||||
"pinkie-promise": "^2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"pify": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
|
||||||
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"picomatch": {
|
|
||||||
"version": "2.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
|
||||||
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
|
|
||||||
},
|
|
||||||
"pinkie": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
|
||||||
"integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
|
|
||||||
},
|
|
||||||
"pinkie-promise": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
|
|
||||||
"integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
|
|
||||||
"requires": {
|
|
||||||
"pinkie": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"read-pkg": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
|
||||||
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
|
|
||||||
"requires": {
|
|
||||||
"load-json-file": "^1.0.0",
|
|
||||||
"normalize-package-data": "^2.3.2",
|
|
||||||
"path-type": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"read-pkg-up": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
|
|
||||||
"requires": {
|
|
||||||
"find-up": "^1.0.0",
|
|
||||||
"read-pkg": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"readdirp": {
|
|
||||||
"version": "3.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
|
|
||||||
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
|
|
||||||
"requires": {
|
|
||||||
"picomatch": "^2.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"redent": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
|
|
||||||
"requires": {
|
|
||||||
"indent-string": "^2.1.0",
|
|
||||||
"strip-indent": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"repeating": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
|
|
||||||
"integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
|
|
||||||
"requires": {
|
|
||||||
"is-finite": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resolve": {
|
|
||||||
"version": "1.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz",
|
|
||||||
"integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==",
|
|
||||||
"requires": {
|
|
||||||
"is-core-module": "^2.0.0",
|
|
||||||
"path-parse": "^1.0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rimraf": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
|
||||||
"requires": {
|
|
||||||
"glob": "^7.1.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"semver": {
|
|
||||||
"version": "5.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
|
||||||
},
|
|
||||||
"signal-exit": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
|
|
||||||
},
|
|
||||||
"source-map": {
|
|
||||||
"version": "0.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
|
||||||
},
|
|
||||||
"source-map-support": {
|
|
||||||
"version": "0.5.19",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
|
|
||||||
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
|
|
||||||
"requires": {
|
|
||||||
"buffer-from": "^1.0.0",
|
|
||||||
"source-map": "^0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"spdx-correct": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
|
|
||||||
"requires": {
|
|
||||||
"spdx-expression-parse": "^3.0.0",
|
|
||||||
"spdx-license-ids": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"spdx-exceptions": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A=="
|
|
||||||
},
|
|
||||||
"spdx-expression-parse": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
|
||||||
"requires": {
|
|
||||||
"spdx-exceptions": "^2.1.0",
|
|
||||||
"spdx-license-ids": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"spdx-license-ids": {
|
|
||||||
"version": "3.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz",
|
|
||||||
"integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw=="
|
|
||||||
},
|
|
||||||
"strip-bom": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
|
|
||||||
"requires": {
|
|
||||||
"is-utf8": "^0.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"strip-indent": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
|
|
||||||
"requires": {
|
|
||||||
"get-stdin": "^4.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"strip-json-comments": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
|
||||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
|
|
||||||
},
|
|
||||||
"to-regex-range": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
|
||||||
"requires": {
|
|
||||||
"is-number": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tree-kill": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
|
|
||||||
},
|
|
||||||
"trim-newlines": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM="
|
|
||||||
},
|
|
||||||
"ts-node": {
|
|
||||||
"version": "9.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
|
|
||||||
"integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==",
|
|
||||||
"requires": {
|
|
||||||
"arg": "^4.1.0",
|
|
||||||
"diff": "^4.0.1",
|
|
||||||
"make-error": "^1.1.1",
|
|
||||||
"source-map-support": "^0.5.17",
|
|
||||||
"yn": "3.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ts-node-dev": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-leA/3TgGtnVU77fGngBwVZztqyDRXirytR7dMtMWZS5b2hGpLl+VDnB0F/gf3A+HEPSzS/KwxgXFP7/LtgX4MQ==",
|
|
||||||
"requires": {
|
|
||||||
"chokidar": "^3.4.0",
|
|
||||||
"dateformat": "~1.0.4-1.2.3",
|
|
||||||
"dynamic-dedupe": "^0.3.0",
|
|
||||||
"minimist": "^1.2.5",
|
|
||||||
"mkdirp": "^1.0.4",
|
|
||||||
"resolve": "^1.0.0",
|
|
||||||
"rimraf": "^2.6.1",
|
|
||||||
"source-map-support": "^0.5.12",
|
|
||||||
"tree-kill": "^1.2.2",
|
|
||||||
"ts-node": "^9.0.0",
|
|
||||||
"tsconfig": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tsconfig": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==",
|
|
||||||
"requires": {
|
|
||||||
"@types/strip-bom": "^3.0.0",
|
|
||||||
"@types/strip-json-comments": "0.0.30",
|
|
||||||
"strip-bom": "^3.0.0",
|
|
||||||
"strip-json-comments": "^2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"strip-bom": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg=="
|
|
||||||
},
|
|
||||||
"validate-npm-package-license": {
|
|
||||||
"version": "3.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
|
||||||
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
|
||||||
"requires": {
|
|
||||||
"spdx-correct": "^3.0.0",
|
|
||||||
"spdx-expression-parse": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"wrappy": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
|
||||||
"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=="
|
|
||||||
},
|
|
||||||
"xtend": {
|
|
||||||
"version": "4.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
|
||||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
|
||||||
},
|
|
||||||
"yn": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"name": "workadventure-artillery",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Load testing for WorkAdventure",
|
|
||||||
"scripts": {
|
|
||||||
"start": "ts-node ./index.ts"
|
|
||||||
},
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"name": "Grégoire Parant",
|
|
||||||
"email": "g.parant@thecodingmachine.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "David Négrier",
|
|
||||||
"email": "d.negrier@thecodingmachine.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Arthmaël Poly",
|
|
||||||
"email": "a.poly@thecodingmachine.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "SEE LICENSE IN LICENSE.txt",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/ws": "^7.2.6",
|
|
||||||
"ts-node-dev": "^1.0.0-pre.62",
|
|
||||||
"typescript": "^4.0.2",
|
|
||||||
"ws": "^7.4.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {}
|
|
||||||
}
|
|
|
@ -1,528 +0,0 @@
|
||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
"@types/node@*":
|
|
||||||
version "14.11.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
|
|
||||||
|
|
||||||
"@types/strip-bom@^3.0.0":
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
|
|
||||||
|
|
||||||
"@types/strip-json-comments@0.0.30":
|
|
||||||
version "0.0.30"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1"
|
|
||||||
|
|
||||||
"@types/ws@^7.2.6":
|
|
||||||
version "7.2.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d"
|
|
||||||
dependencies:
|
|
||||||
"@types/node" "*"
|
|
||||||
|
|
||||||
anymatch@~3.1.1:
|
|
||||||
version "3.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
|
|
||||||
dependencies:
|
|
||||||
normalize-path "^3.0.0"
|
|
||||||
picomatch "^2.0.4"
|
|
||||||
|
|
||||||
arg@^4.1.0:
|
|
||||||
version "4.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
balanced-match@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
|
||||||
|
|
||||||
binary-extensions@^2.0.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
|
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
|
||||||
version "1.1.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
|
||||||
dependencies:
|
|
||||||
balanced-match "^1.0.0"
|
|
||||||
concat-map "0.0.1"
|
|
||||||
|
|
||||||
braces@~3.0.2:
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
|
||||||
dependencies:
|
|
||||||
fill-range "^7.0.1"
|
|
||||||
|
|
||||||
buffer-from@^1.0.0:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
|
||||||
|
|
||||||
camelcase-keys@^2.0.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
|
|
||||||
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"
|
|
||||||
|
|
||||||
chokidar@^3.4.0:
|
|
||||||
version "3.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d"
|
|
||||||
dependencies:
|
|
||||||
anymatch "~3.1.1"
|
|
||||||
braces "~3.0.2"
|
|
||||||
glob-parent "~5.1.0"
|
|
||||||
is-binary-path "~2.1.0"
|
|
||||||
is-glob "~4.0.1"
|
|
||||||
normalize-path "~3.0.0"
|
|
||||||
readdirp "~3.4.0"
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents "~2.1.2"
|
|
||||||
|
|
||||||
concat-map@0.0.1:
|
|
||||||
version "0.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
|
||||||
|
|
||||||
currently-unhandled@^0.4.1:
|
|
||||||
version "0.4.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
|
|
||||||
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"
|
|
||||||
dependencies:
|
|
||||||
get-stdin "^4.0.1"
|
|
||||||
meow "^3.3.0"
|
|
||||||
|
|
||||||
decamelize@^1.1.2:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
|
||||||
|
|
||||||
diff@^4.0.1:
|
|
||||||
version "4.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
|
||||||
|
|
||||||
dynamic-dedupe@^0.3.0:
|
|
||||||
version "0.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1"
|
|
||||||
dependencies:
|
|
||||||
xtend "^4.0.0"
|
|
||||||
|
|
||||||
error-ex@^1.2.0:
|
|
||||||
version "1.3.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
|
|
||||||
dependencies:
|
|
||||||
is-arrayish "^0.2.1"
|
|
||||||
|
|
||||||
fill-range@^7.0.1:
|
|
||||||
version "7.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
|
||||||
dependencies:
|
|
||||||
to-regex-range "^5.0.1"
|
|
||||||
|
|
||||||
find-up@^1.0.0:
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
|
|
||||||
dependencies:
|
|
||||||
path-exists "^2.0.0"
|
|
||||||
pinkie-promise "^2.0.0"
|
|
||||||
|
|
||||||
fs.realpath@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
|
||||||
|
|
||||||
fsevents@~2.1.2:
|
|
||||||
version "2.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
|
|
||||||
|
|
||||||
get-stdin@^4.0.1:
|
|
||||||
version "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"
|
|
||||||
dependencies:
|
|
||||||
is-glob "^4.0.1"
|
|
||||||
|
|
||||||
glob@^7.1.3:
|
|
||||||
version "7.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
|
||||||
dependencies:
|
|
||||||
fs.realpath "^1.0.0"
|
|
||||||
inflight "^1.0.4"
|
|
||||||
inherits "2"
|
|
||||||
minimatch "^3.0.4"
|
|
||||||
once "^1.3.0"
|
|
||||||
path-is-absolute "^1.0.0"
|
|
||||||
|
|
||||||
graceful-fs@^4.1.2:
|
|
||||||
version "4.2.4"
|
|
||||||
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"
|
|
||||||
|
|
||||||
indent-string@^2.1.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
|
|
||||||
dependencies:
|
|
||||||
repeating "^2.0.0"
|
|
||||||
|
|
||||||
inflight@^1.0.4:
|
|
||||||
version "1.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
|
||||||
dependencies:
|
|
||||||
once "^1.3.0"
|
|
||||||
wrappy "1"
|
|
||||||
|
|
||||||
inherits@2:
|
|
||||||
version "2.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
|
||||||
|
|
||||||
is-arrayish@^0.2.1:
|
|
||||||
version "0.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
|
||||||
|
|
||||||
is-binary-path@~2.1.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
|
||||||
dependencies:
|
|
||||||
binary-extensions "^2.0.0"
|
|
||||||
|
|
||||||
is-extglob@^2.1.1:
|
|
||||||
version "2.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
|
||||||
|
|
||||||
is-finite@^1.0.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
|
|
||||||
|
|
||||||
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"
|
|
||||||
dependencies:
|
|
||||||
is-extglob "^2.1.1"
|
|
||||||
|
|
||||||
is-number@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
|
||||||
|
|
||||||
is-utf8@^0.2.0:
|
|
||||||
version "0.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
|
||||||
|
|
||||||
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"
|
|
||||||
dependencies:
|
|
||||||
graceful-fs "^4.1.2"
|
|
||||||
parse-json "^2.2.0"
|
|
||||||
pify "^2.0.0"
|
|
||||||
pinkie-promise "^2.0.0"
|
|
||||||
strip-bom "^2.0.0"
|
|
||||||
|
|
||||||
loud-rejection@^1.0.0:
|
|
||||||
version "1.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
|
|
||||||
dependencies:
|
|
||||||
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"
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
meow@^3.3.0:
|
|
||||||
version "3.7.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
|
|
||||||
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"
|
|
||||||
|
|
||||||
minimatch@^3.0.4:
|
|
||||||
version "3.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
|
||||||
dependencies:
|
|
||||||
brace-expansion "^1.1.7"
|
|
||||||
|
|
||||||
minimist@^1.1.3, minimist@^1.2.5:
|
|
||||||
version "1.2.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
|
||||||
|
|
||||||
mkdirp@^1.0.4:
|
|
||||||
version "1.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
|
||||||
|
|
||||||
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"
|
|
||||||
dependencies:
|
|
||||||
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"
|
|
||||||
|
|
||||||
object-assign@^4.0.1:
|
|
||||||
version "4.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
|
||||||
|
|
||||||
once@^1.3.0:
|
|
||||||
version "1.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
|
||||||
dependencies:
|
|
||||||
wrappy "1"
|
|
||||||
|
|
||||||
parse-json@^2.2.0:
|
|
||||||
version "2.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
|
|
||||||
dependencies:
|
|
||||||
error-ex "^1.2.0"
|
|
||||||
|
|
||||||
path-exists@^2.0.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
|
|
||||||
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"
|
|
||||||
|
|
||||||
path-parse@^1.0.6:
|
|
||||||
version "1.0.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
|
||||||
|
|
||||||
path-type@^1.0.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
|
|
||||||
dependencies:
|
|
||||||
graceful-fs "^4.1.2"
|
|
||||||
pify "^2.0.0"
|
|
||||||
pinkie-promise "^2.0.0"
|
|
||||||
|
|
||||||
picomatch@^2.0.4, picomatch@^2.2.1:
|
|
||||||
version "2.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
|
|
||||||
|
|
||||||
pify@^2.0.0:
|
|
||||||
version "2.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
|
||||||
|
|
||||||
pinkie-promise@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
|
|
||||||
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"
|
|
||||||
|
|
||||||
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"
|
|
||||||
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"
|
|
||||||
dependencies:
|
|
||||||
load-json-file "^1.0.0"
|
|
||||||
normalize-package-data "^2.3.2"
|
|
||||||
path-type "^1.0.0"
|
|
||||||
|
|
||||||
readdirp@~3.4.0:
|
|
||||||
version "3.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada"
|
|
||||||
dependencies:
|
|
||||||
picomatch "^2.2.1"
|
|
||||||
|
|
||||||
redent@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
|
|
||||||
dependencies:
|
|
||||||
indent-string "^2.1.0"
|
|
||||||
strip-indent "^1.0.1"
|
|
||||||
|
|
||||||
repeating@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
|
|
||||||
dependencies:
|
|
||||||
is-finite "^1.0.0"
|
|
||||||
|
|
||||||
resolve@^1.0.0, resolve@^1.10.0:
|
|
||||||
version "1.17.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
|
|
||||||
dependencies:
|
|
||||||
path-parse "^1.0.6"
|
|
||||||
|
|
||||||
rimraf@^2.6.1:
|
|
||||||
version "2.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
|
||||||
dependencies:
|
|
||||||
glob "^7.1.3"
|
|
||||||
|
|
||||||
"semver@2 || 3 || 4 || 5":
|
|
||||||
version "5.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
|
||||||
|
|
||||||
signal-exit@^3.0.0:
|
|
||||||
version "3.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
|
||||||
|
|
||||||
source-map-support@^0.5.12, source-map-support@^0.5.17:
|
|
||||||
version "0.5.19"
|
|
||||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
|
||||||
dependencies:
|
|
||||||
buffer-from "^1.0.0"
|
|
||||||
source-map "^0.6.0"
|
|
||||||
|
|
||||||
source-map@^0.6.0:
|
|
||||||
version "0.6.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
|
||||||
|
|
||||||
spdx-correct@^3.0.0:
|
|
||||||
version "3.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
|
||||||
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"
|
|
||||||
|
|
||||||
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"
|
|
||||||
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"
|
|
||||||
|
|
||||||
strip-bom@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
|
|
||||||
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"
|
|
||||||
|
|
||||||
strip-indent@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
|
|
||||||
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"
|
|
||||||
|
|
||||||
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"
|
|
||||||
dependencies:
|
|
||||||
is-number "^7.0.0"
|
|
||||||
|
|
||||||
tree-kill@^1.2.2:
|
|
||||||
version "1.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
|
||||||
|
|
||||||
trim-newlines@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
|
||||||
|
|
||||||
ts-node-dev@^1.0.0-pre.62:
|
|
||||||
version "1.0.0-pre.62"
|
|
||||||
resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.62.tgz#835644c43669b659a880379b9d06df86cef665ad"
|
|
||||||
dependencies:
|
|
||||||
chokidar "^3.4.0"
|
|
||||||
dateformat "~1.0.4-1.2.3"
|
|
||||||
dynamic-dedupe "^0.3.0"
|
|
||||||
minimist "^1.2.5"
|
|
||||||
mkdirp "^1.0.4"
|
|
||||||
resolve "^1.0.0"
|
|
||||||
rimraf "^2.6.1"
|
|
||||||
source-map-support "^0.5.12"
|
|
||||||
tree-kill "^1.2.2"
|
|
||||||
ts-node "^8.10.2"
|
|
||||||
tsconfig "^7.0.0"
|
|
||||||
|
|
||||||
ts-node@^8.10.2:
|
|
||||||
version "8.10.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
|
|
||||||
dependencies:
|
|
||||||
arg "^4.1.0"
|
|
||||||
diff "^4.0.1"
|
|
||||||
make-error "^1.1.1"
|
|
||||||
source-map-support "^0.5.17"
|
|
||||||
yn "3.1.1"
|
|
||||||
|
|
||||||
tsconfig@^7.0.0:
|
|
||||||
version "7.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7"
|
|
||||||
dependencies:
|
|
||||||
"@types/strip-bom" "^3.0.0"
|
|
||||||
"@types/strip-json-comments" "0.0.30"
|
|
||||||
strip-bom "^3.0.0"
|
|
||||||
strip-json-comments "^2.0.0"
|
|
||||||
|
|
||||||
typescript@^4.0.2:
|
|
||||||
version "4.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
|
|
||||||
|
|
||||||
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"
|
|
||||||
dependencies:
|
|
||||||
spdx-correct "^3.0.0"
|
|
||||||
spdx-expression-parse "^3.0.0"
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
xtend@^4.0.0:
|
|
||||||
version "4.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
|
||||||
|
|
||||||
yn@3.1.1:
|
|
||||||
version "3.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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"
|
|
|
@ -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
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue