Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
ea9d12b74e |
|
@ -1,2 +0,0 @@
|
|||
**/node_modules/**
|
||||
**/Dockerfile
|
|
@ -5,27 +5,3 @@ JITSI_PRIVATE_MODE=false
|
|||
JITSI_ISS=
|
||||
SECRET_JITSI_KEY=
|
||||
ADMIN_API_TOKEN=123
|
||||
START_ROOM_URL=/_/global/maps.workadventure.localhost/starter/map.json
|
||||
# If your Turn server is configured to use the Turn REST API, you should put the shared auth secret here.
|
||||
# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
|
||||
# Keep empty if you are sharing hard coded / clear text credentials.
|
||||
TURN_STATIC_AUTH_SECRET=
|
||||
DISABLE_NOTIFICATIONS=true
|
||||
SKIP_RENDER_OPTIMIZATIONS=false
|
||||
|
||||
# The email address used by Let's encrypt to send renewal warnings (compulsory)
|
||||
ACME_EMAIL=
|
||||
|
||||
MAX_PER_GROUP=4
|
||||
MAX_USERNAME_LENGTH=8
|
||||
|
||||
OPID_CLIENT_ID=
|
||||
OPID_CLIENT_SECRET=
|
||||
OPID_CLIENT_ISSUER=
|
||||
OPID_CLIENT_REDIRECT_URL=
|
||||
OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen
|
||||
OPID_PROFILE_SCREEN_PROVIDER=
|
||||
DISABLE_ANONYMOUS=
|
||||
|
||||
# If you want to have a contact page in your menu, you MUST set CONTACT_URL to the URL of the page that you want
|
||||
CONTACT_URL=
|
142
.github/workflows/build-and-deploy.yml
vendored
142
.github/workflows/build-and-deploy.yml
vendored
|
@ -1,13 +1,7 @@
|
|||
name: Build, push and deploy Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
release:
|
||||
types: [created]
|
||||
pull_request:
|
||||
types: [ labeled, synchronize ]
|
||||
|
||||
- push
|
||||
|
||||
# Enables BuildKit
|
||||
env:
|
||||
|
@ -16,7 +10,7 @@ env:
|
|||
jobs:
|
||||
|
||||
build-front:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -26,7 +20,7 @@ jobs:
|
|||
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@3.1.0
|
||||
- uses: rlespinasse/github-slug-action@1.1.1
|
||||
|
||||
- name: "Build and push front image"
|
||||
uses: docker/build-push-action@v1
|
||||
|
@ -36,11 +30,11 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: thecodingmachine/workadventure-front
|
||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||
add_git_labels: true
|
||||
|
||||
build-back:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -49,7 +43,7 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@3.1.0
|
||||
- uses: rlespinasse/github-slug-action@1.1.1
|
||||
|
||||
- name: "Build and push back image"
|
||||
uses: docker/build-push-action@v1
|
||||
|
@ -59,11 +53,11 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: thecodingmachine/workadventure-back
|
||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||
add_git_labels: true
|
||||
|
||||
build-pusher:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
build-website:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -72,44 +66,21 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@3.1.0
|
||||
- uses: rlespinasse/github-slug-action@1.1.1
|
||||
|
||||
- name: "Build and push back image"
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
dockerfile: pusher/Dockerfile
|
||||
path: ./
|
||||
dockerfile: website/Dockerfile
|
||||
path: website/
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: thecodingmachine/workadventure-pusher
|
||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
add_git_labels: true
|
||||
|
||||
build-uploader:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@3.1.0
|
||||
|
||||
- name: "Build and push back image"
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
dockerfile: uploader/Dockerfile
|
||||
path: ./
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: thecodingmachine/workadventure-uploader
|
||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
repository: thecodingmachine/workadventure-website
|
||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||
add_git_labels: true
|
||||
|
||||
build-maps:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -119,7 +90,7 @@ jobs:
|
|||
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@3.1.0
|
||||
- uses: rlespinasse/github-slug-action@1.1.1
|
||||
|
||||
- name: "Build and push front image"
|
||||
uses: docker/build-push-action@v1
|
||||
|
@ -129,76 +100,67 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: thecodingmachine/workadventure-maps
|
||||
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
||||
add_git_labels: true
|
||||
|
||||
deeploy:
|
||||
needs:
|
||||
- build-front
|
||||
- build-back
|
||||
- build-pusher
|
||||
- build-maps
|
||||
- build-uploader
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@3.1.0
|
||||
|
||||
- name: Write certificate
|
||||
run: echo "${CERTS_PRIVATE_KEY}" > secret.key && chmod 0600 secret.key
|
||||
env:
|
||||
CERTS_PRIVATE_KEY: ${{ secrets.CERTS_PRIVATE_KEY }}
|
||||
|
||||
- name: Download certificate
|
||||
run: mkdir secrets && scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i secret.key ubuntu@cert.workadventu.re:./config/live/workadventu.re/* secrets/
|
||||
|
||||
- name: Create namespace
|
||||
uses: steebchen/kubectl@v1.0.0
|
||||
env:
|
||||
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||
with:
|
||||
args: create namespace workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Delete old certificates in namespace
|
||||
uses: steebchen/kubectl@v1.0.0
|
||||
env:
|
||||
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||
with:
|
||||
args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} delete secret certificate-tls
|
||||
continue-on-error: true
|
||||
|
||||
- name: Install certificates in namespace
|
||||
uses: steebchen/kubectl@v1.0.0
|
||||
env:
|
||||
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||
with:
|
||||
args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} create secret tls certificate-tls --key="secrets/privkey.pem" --cert="secrets/fullchain.pem"
|
||||
- uses: rlespinasse/github-slug-action@1.1.0
|
||||
|
||||
- name: Deploy
|
||||
uses: thecodingmachine/deeployer-action@master
|
||||
uses: thecodingmachine/deeployer@master
|
||||
env:
|
||||
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
||||
ADMIN_API_TOKEN: ${{ secrets.ADMIN_API_TOKEN }}
|
||||
JITSI_ISS: ${{ secrets.JITSI_ISS }}
|
||||
JITSI_URL: ${{ secrets.JITSI_URL }}
|
||||
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
||||
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
|
||||
DEPLOY_REF: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
|
||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||
with:
|
||||
namespace: workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||
namespace: workadventure-${{ env.GITHUB_REF_SLUG }}
|
||||
|
||||
- name: Add a comment in PR
|
||||
uses: unsplash/comment-on-pr@v1.2.0
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
if: ${{ env.GITHUB_REF_SLUG != 'master' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
msg: "Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re \nTests available at https://maps-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re/tests"
|
||||
msg: Environment deployed at https://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||
check_for_duplicate_msg: true
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v2
|
||||
if: ${{ env.GITHUB_REF_SLUG != 'master' }}
|
||||
env:
|
||||
CYPRESS_BASE_URL: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||
with:
|
||||
env: host=play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com,port=80
|
||||
spec: cypress/integration/spec.js
|
||||
wait-on: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||
working-directory: e2e
|
||||
|
||||
- name: Run Cypress tests in prod
|
||||
uses: cypress-io/github-action@v2
|
||||
if: ${{ env.GITHUB_REF_SLUG == 'master' }}
|
||||
env:
|
||||
CYPRESS_BASE_URL: https://play.workadventu.re
|
||||
with:
|
||||
env: host=play.workadventu.re
|
||||
spec: cypress/integration/spec.js
|
||||
wait-on: https://workadventu.re
|
||||
working-directory: e2e
|
||||
|
||||
- name: "Upload the screenshot on test failure"
|
||||
uses: actions/upload-artifact@v1
|
||||
if: failure()
|
||||
with:
|
||||
name: "screenshot"
|
||||
path: "./e2e/cypress/screenshots/spec.js/WorkAdventureGame -- loads (failed).png"
|
||||
|
|
10
.github/workflows/cleanup.yml
vendored
10
.github/workflows/cleanup.yml
vendored
|
@ -1,8 +1,7 @@
|
|||
name: Cleanup images and environments
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
- delete
|
||||
|
||||
# Enables BuildKit
|
||||
env:
|
||||
|
@ -15,12 +14,13 @@ jobs:
|
|||
|
||||
steps:
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@3.1.0
|
||||
- uses: rlespinasse/github-slug-action@1.1.0
|
||||
|
||||
- name: Cleanup
|
||||
continue-on-error: true
|
||||
uses: thecodingmachine/deeployer-cleanup-action@master
|
||||
env:
|
||||
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
||||
with:
|
||||
namespace: workadventure-${{ env.GITHUB_HEAD_REF_SLUG }}
|
||||
# FIXME: we are not using ${{ env.GITHUB_REF_SLUG }} that resolves to master BUT! we are not using a slugified namespace
|
||||
# so complex namespace names will not be treated correctly
|
||||
namespace: workadventure-${{ github.event.ref }}
|
||||
|
|
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
|
80
.github/workflows/continuous_integration.yml
vendored
80
.github/workflows/continuous_integration.yml
vendored
|
@ -3,13 +3,11 @@
|
|||
name: "Continuous Integration"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
- "pull_request"
|
||||
- "push"
|
||||
|
||||
jobs:
|
||||
|
||||
continuous-integration-front:
|
||||
name: "Continuous Integration Front"
|
||||
|
||||
|
@ -38,87 +36,23 @@ jobs:
|
|||
working-directory: "messages"
|
||||
|
||||
- name: "Build proto messages"
|
||||
run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
|
||||
run: yarn run proto && yarn run copy-to-front
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Create index.html"
|
||||
run: ./templater.sh
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Generate i18n files"
|
||||
run: yarn run typesafe-i18n
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Build"
|
||||
run: yarn run build
|
||||
env:
|
||||
PUSHER_URL: "//localhost:8080"
|
||||
ADMIN_URL: "//localhost:80"
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Svelte check"
|
||||
run: yarn run svelte-check
|
||||
API_URL: "localhost:8080"
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Lint"
|
||||
run: yarn run lint
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Pretty"
|
||||
run: yarn run pretty-check
|
||||
working-directory: "front"
|
||||
|
||||
- name: "Jasmine"
|
||||
run: yarn test
|
||||
working-directory: "front"
|
||||
|
||||
continuous-integration-pusher:
|
||||
name: "Continuous Integration Pusher"
|
||||
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v2.0.0"
|
||||
|
||||
- name: "Setup NodeJS"
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v1
|
||||
with:
|
||||
version: '3.x'
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: yarn install
|
||||
working-directory: "pusher"
|
||||
|
||||
- name: "Install messages dependencies"
|
||||
run: yarn install
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build proto messages"
|
||||
run: yarn run proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher
|
||||
working-directory: "messages"
|
||||
|
||||
- name: "Build"
|
||||
run: yarn run tsc
|
||||
working-directory: "pusher"
|
||||
|
||||
- name: "Lint"
|
||||
run: yarn run lint
|
||||
working-directory: "pusher"
|
||||
|
||||
- name: "Jasmine"
|
||||
run: yarn test
|
||||
working-directory: "pusher"
|
||||
|
||||
- name: "Prettier"
|
||||
run: yarn run pretty-check
|
||||
working-directory: "pusher"
|
||||
|
||||
continuous-integration-back:
|
||||
name: "Continuous Integration Back"
|
||||
|
||||
|
@ -162,7 +96,3 @@ jobs:
|
|||
run: yarn test
|
||||
working-directory: "back"
|
||||
|
||||
- name: "Prettier"
|
||||
run: yarn run pretty-check
|
||||
working-directory: "back"
|
||||
|
||||
|
|
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' }}
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -3,9 +3,4 @@
|
|||
.vagrant
|
||||
Vagrantfile
|
||||
docker-compose.override.yaml
|
||||
*.DS_Store
|
||||
maps/yarn.lock
|
||||
maps/dist/computer.js
|
||||
maps/dist/computer.js.map
|
||||
node_modules
|
||||
_
|
||||
*.DS_Store
|
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.
|
BIN
README-INTRO.jpg
Normal file
BIN
README-INTRO.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 386 KiB |
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 |
40
README.md
40
README.md
|
@ -1,49 +1,41 @@
|
|||
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg) [![Discord](https://img.shields.io/discord/821338762134290432?label=Discord)](https://discord.gg/YGtngdh9gt)
|
||||
![](https://github.com/thecodingmachine/workadventure/workflows/Continuous%20Integration/badge.svg)
|
||||
|
||||
![WorkAdventure logo](README-LOGO.svg)
|
||||
![WorkAdventure office image](README-MAP.png)
|
||||
![WorkAdventure landscape image](README-INTRO.jpg)
|
||||
|
||||
Live demo [here](https://play.workadventu.re/@/tcm/workadventure/wa-village).
|
||||
Demo here : [https://workadventu.re/](https://workadventu.re/).
|
||||
|
||||
# WorkAdventure
|
||||
# Work Adventure
|
||||
|
||||
WorkAdventure is a web-based collaborative workspace presented in the form of a
|
||||
## Work in progress
|
||||
|
||||
Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a
|
||||
16-bit video game.
|
||||
|
||||
In WorkAdventure you can move around your office and talk to your colleagues (using a video-chat system, triggered when you approach someone).
|
||||
In Work Adventure, you can move around your office and talk to your colleagues (using a video-chat feature that is
|
||||
triggered when you move next to a colleague).
|
||||
|
||||
See more features for your virtual office: https://workadventu.re/virtual-office
|
||||
|
||||
## Community resources
|
||||
|
||||
Check out resources developed by the WorkAdventure community at [awesome-workadventure](https://github.com/workadventure/awesome-workadventure)
|
||||
|
||||
## Setting up a development environment
|
||||
## Getting started
|
||||
|
||||
Install Docker.
|
||||
|
||||
Run:
|
||||
|
||||
```
|
||||
cp .env.template .env
|
||||
docker-compose up -d
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
The environment will start.
|
||||
|
||||
You should now be able to browse to http://play.workadventure.localhost/ and see the application.
|
||||
You can view the dashboard at http://workadventure.localhost:8080/
|
||||
You should now be able to browse to http://workadventure.localhost/ and see the application.
|
||||
|
||||
Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
|
||||
|
||||
**/etc/hosts**
|
||||
```
|
||||
127.0.0.1 workadventure.localhost
|
||||
workadventure.localhost 127.0.0.1
|
||||
```
|
||||
|
||||
Note: If on the first run you get a page with "network error". Try to ``docker-compose stop`` , then ``docker-compose start``.
|
||||
Note 2: If you are still getting "network error". Make sure you are authorizing the self-signed certificate by entering https://pusher.workadventure.localhost and accepting them.
|
||||
|
||||
### MacOS developers, your environment with Vagrant
|
||||
|
||||
If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c).
|
||||
|
@ -109,7 +101,5 @@ Vagrant destroy
|
|||
* `Vagrant halt`: stop your VM Vagrant.
|
||||
* `Vagrant destroy`: delete your VM Vagrant.
|
||||
|
||||
## Setting up a production environment
|
||||
|
||||
The way you set up your production environment will highly depend on your servers.
|
||||
We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) directory.
|
||||
## Features developed
|
||||
You have more details of features developed in back [README.md](./back/README.md).
|
||||
|
|
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 -*-
|
||||
# vi: set ft=ruby :
|
||||
# Box / OS
|
||||
VAGRANT_BOX = 'bento/ubuntu-20.04'
|
||||
VAGRANT_BOX = 'bento/ubuntu-19.10'
|
||||
|
||||
# VM User — 'vagrant' by default
|
||||
VM_USER = 'vagrant'
|
||||
|
@ -58,7 +58,7 @@ Vagrant.configure(2) do |config|
|
|||
apt-get update -y
|
||||
apt-get install -y git
|
||||
apt-get install -y apt-transport-https
|
||||
apt-get install -y ca-certificates
|
||||
apt-get install -y build-essential
|
||||
apt-get install -y curl
|
||||
apt-get install -y gnupg-agent
|
||||
apt-get install -y software-properties-common
|
||||
|
@ -66,8 +66,8 @@ Vagrant.configure(2) do |config|
|
|||
apt-key fingerprint 0EBFCD88
|
||||
add-apt-repository \
|
||||
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
|
||||
$(lsb_release -cs) \
|
||||
stable"
|
||||
$(lsb_release -cs) \
|
||||
stable"
|
||||
apt-get update -y
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||
curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"no-throw-literal": "error"
|
||||
"@typescript-eslint/no-explicit-any": "error"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
src/Messages/generated
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4
|
||||
}
|
|
@ -1,26 +1,15 @@
|
|||
# protobuf build
|
||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder
|
||||
WORKDIR /usr/src
|
||||
COPY messages .
|
||||
FROM thecodingmachine/workadventure-back-base:latest as builder
|
||||
WORKDIR /var/www/messages
|
||||
COPY --chown=docker:docker messages .
|
||||
RUN yarn install && yarn proto
|
||||
|
||||
# typescript build
|
||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d as builder2
|
||||
WORKDIR /usr/src
|
||||
COPY back/yarn.lock back/package.json ./
|
||||
FROM thecodingmachine/nodejs:12
|
||||
|
||||
COPY --chown=docker:docker back .
|
||||
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated
|
||||
RUN yarn install
|
||||
COPY back .
|
||||
COPY --from=builder /usr/src/generated src/Messages/generated
|
||||
ENV NODE_ENV=production
|
||||
RUN yarn run tsc
|
||||
|
||||
# final production image
|
||||
FROM node:14.18.2-buster-slim@sha256:20bedf0c09de887379e59a41c04284974f5fb529cf0e13aab613473ce298da3d
|
||||
WORKDIR /usr/src
|
||||
COPY back/yarn.lock back/package.json ./
|
||||
COPY --from=builder2 /usr/src/dist /usr/src/dist
|
||||
ENV NODE_ENV=production
|
||||
RUN yarn install --production
|
||||
|
||||
USER node
|
||||
CMD ["yarn", "run", "runprod"]
|
||||
CMD ["yarn", "run", "prod"]
|
||||
|
||||
|
|
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)
|
|
@ -7,14 +7,10 @@
|
|||
"tsc": "tsc",
|
||||
"dev": "ts-node-dev --respawn ./server.ts",
|
||||
"prod": "tsc && node --max-old-space-size=4096 ./dist/server.js",
|
||||
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
||||
"profile": "tsc && node --prof ./dist/server.js",
|
||||
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||
"lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts",
|
||||
"fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts",
|
||||
"precommit": "lint-staged",
|
||||
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
||||
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
||||
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
||||
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -40,46 +36,37 @@
|
|||
},
|
||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||
"dependencies": {
|
||||
"@workadventure/tiled-map-type-guard": "^1.0.3",
|
||||
"axios": "^0.21.2",
|
||||
"axios": "^0.20.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"busboy": "^0.3.1",
|
||||
"circular-json": "^0.5.9",
|
||||
"debug": "^4.3.1",
|
||||
"generic-type-guard": "^3.2.0",
|
||||
"google-protobuf": "^3.13.0",
|
||||
"grpc": "^1.24.4",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"http-status-codes": "^1.4.0",
|
||||
"iterall": "^1.3.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"multer": "^1.4.2",
|
||||
"prom-client": "^12.0.0",
|
||||
"query-string": "^6.13.3",
|
||||
"redis": "^3.1.2",
|
||||
"systeminformation": "^4.27.11",
|
||||
"ts-node-dev": "^1.0.0-pre.44",
|
||||
"typescript": "^3.8.3",
|
||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||
"uuidv4": "^6.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/busboy": "^0.2.3",
|
||||
"@types/circular-json": "^0.4.0",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/google-protobuf": "^3.7.3",
|
||||
"@types/http-status-codes": "^1.2.0",
|
||||
"@types/jasmine": "^3.5.10",
|
||||
"@types/jsonwebtoken": "^8.3.8",
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/redis": "^2.8.31",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
||||
"@typescript-eslint/parser": "^5.8.0",
|
||||
"eslint": "^8.5.0",
|
||||
"jasmine": "^3.5.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"prettier": "^2.3.1",
|
||||
"ts-node-dev": "^1.1.8",
|
||||
"typescript": "^4.5.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": [
|
||||
"prettier --write"
|
||||
]
|
||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||
"@typescript-eslint/parser": "^2.26.0",
|
||||
"eslint": "^6.8.0",
|
||||
"jasmine": "^3.5.0"
|
||||
}
|
||||
}
|
||||
|
|
148
back/position-test.js
Normal file
148
back/position-test.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
// Constants
|
||||
let MIN_DISTANCE = 12;
|
||||
let MAX_PER_GROUP = 3;
|
||||
let NB_USERS = 10;
|
||||
|
||||
// Utils
|
||||
let rand = function(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
let compareDistances = function(distA, distB) {
|
||||
if (distA.distance < distB.distance) {
|
||||
return -1;
|
||||
}
|
||||
if (distA.distance > distB.distance) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
let computeDistance = function (user1, user2) {
|
||||
return Math.sqrt(Math.pow(user2.X - user1.X, 2) + Math.pow(user2.Y - user1.Y, 2));
|
||||
};
|
||||
|
||||
// Test Data
|
||||
let users = [];
|
||||
for(let i = 1; i <= NB_USERS; i++) {
|
||||
let user = {};
|
||||
user.id = rand(0,99999);
|
||||
user.X = rand(0, 40);
|
||||
user.Y = rand(0, 40);
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
// Compute distance between each user
|
||||
let getDistanceOfEachUser = function(users) {
|
||||
let i = 0;
|
||||
let distances = [];
|
||||
|
||||
users.forEach(function(user1, key1) {
|
||||
users.forEach(function(user2, key2) {
|
||||
if(key1 < key2) {
|
||||
let distanceObj = {};
|
||||
distanceObj.distance = computeDistance(user1, user2);
|
||||
distanceObj.first = user1;
|
||||
distanceObj.second = user2;
|
||||
|
||||
distances[i] = distanceObj;
|
||||
i++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return distances;
|
||||
};
|
||||
|
||||
// Organise groups
|
||||
let createGroups = function(distances) {
|
||||
let i = 0;
|
||||
let groups = [];
|
||||
let alreadyInAGroup = [];
|
||||
|
||||
for(let j = 0; j < distances.length; j++) {
|
||||
let dist = distances[j];
|
||||
|
||||
if(dist.distance <= MIN_DISTANCE) {
|
||||
if(typeof groups[i] === 'undefined') {
|
||||
groups[i] = [];
|
||||
}
|
||||
|
||||
if(groups[i].indexOf(dist.first) === -1 && typeof alreadyInAGroup[dist.first.id] === 'undefined') {
|
||||
if(groups[i].length > 1) {
|
||||
// if group is not empty we check current user can be added in the group according to its distance to the others already in it
|
||||
for(let l = 0; l < groups[i].length; l++) {
|
||||
let userTotest = groups[i][l];
|
||||
if(computeDistance(dist.first, userTotest) <= MIN_DISTANCE) {
|
||||
groups[i].push(dist.first);
|
||||
alreadyInAGroup[dist.first.id] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
groups[i].push(dist.first);
|
||||
alreadyInAGroup[dist.first.id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(groups[i].length === MAX_PER_GROUP) {
|
||||
i++; // on créé un nouveau groupe
|
||||
if(i > (NB_USERS / MAX_PER_GROUP)) {
|
||||
console.log('There is no room left for user ID : ' + dist.second.id + ' !');
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(groups[i].indexOf(dist.second) === -1 && typeof alreadyInAGroup[dist.second.id] === 'undefined') {
|
||||
if(groups[i].length > 1) {
|
||||
// if group is not empty we check current user can be added in the group according to its distance to the others already in it
|
||||
for(let l = 0; l < groups[i].length; l++) {
|
||||
let userTotest = groups[i][l];
|
||||
if(computeDistance(dist.second, userTotest) <= MIN_DISTANCE) {
|
||||
groups[i].push(dist.second);
|
||||
alreadyInAGroup[dist.second.id] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
groups[i].push(dist.second);
|
||||
alreadyInAGroup[dist.second.id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
let distances = getDistanceOfEachUser(users);
|
||||
|
||||
// ordonner par distance pour prioriser l'association en groupe des utilisateurs les plus proches
|
||||
distances.sort(compareDistances);
|
||||
|
||||
let groups = createGroups(distances);
|
||||
|
||||
// Compute distance between each user of a already existing group
|
||||
let checkGroupDistance = function(groups) {
|
||||
for(let i = 0; i < groups.length; i++) {
|
||||
let group = groups[i];
|
||||
group.forEach(function(user1, key1) {
|
||||
group.forEach(function(user2, key2) {
|
||||
if(key1 < key2) {
|
||||
let distance = computeDistance(user1, user2);
|
||||
if(distance > MIN_DISTANCE) {
|
||||
// TODO : message a user1 et user2
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
console.log(users);
|
||||
console.log(distances);
|
||||
console.log(groups);
|
||||
|
|
@ -1,15 +1,3 @@
|
|||
// lib/server.ts
|
||||
import App from "./src/App";
|
||||
import grpc from "grpc";
|
||||
import { roomManager } from "./src/RoomManager";
|
||||
import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb";
|
||||
import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable";
|
||||
|
||||
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT));
|
||||
|
||||
const server = new grpc.Server();
|
||||
server.addService<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);
|
||||
App.listen(8080, () => console.log(`WorkAdventure starting on port 8080!`))
|
||||
|
|
|
@ -1,16 +1,29 @@
|
|||
// lib/app.ts
|
||||
import { PrometheusController } from "./Controller/PrometheusController";
|
||||
import { DebugController } from "./Controller/DebugController";
|
||||
import { App as uwsApp } from "./Server/sifrr.server";
|
||||
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
|
||||
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
|
||||
import {MapController} from "./Controller/MapController";
|
||||
import {PrometheusController} from "./Controller/PrometheusController";
|
||||
import {FileController} from "./Controller/FileController";
|
||||
import {DebugController} from "./Controller/DebugController";
|
||||
import {App as uwsApp} from "./Server/sifrr.server";
|
||||
|
||||
class App {
|
||||
public app: uwsApp;
|
||||
public ioSocketController: IoSocketController;
|
||||
public authenticateController: AuthenticateController;
|
||||
public fileController: FileController;
|
||||
public mapController: MapController;
|
||||
public prometheusController: PrometheusController;
|
||||
private debugController: DebugController;
|
||||
|
||||
constructor() {
|
||||
this.app = new uwsApp();
|
||||
|
||||
//create socket controllers
|
||||
this.ioSocketController = new IoSocketController(this.app);
|
||||
this.authenticateController = new AuthenticateController(this.app);
|
||||
this.fileController = new FileController(this.app);
|
||||
this.mapController = new MapController(this.app);
|
||||
this.prometheusController = new PrometheusController(this.app);
|
||||
this.debugController = new DebugController(this.app);
|
||||
}
|
||||
|
|
135
back/src/Controller/AuthenticateController.ts
Normal file
135
back/src/Controller/AuthenticateController.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
import { v4 } from 'uuid';
|
||||
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
|
||||
import {BaseController} from "./BaseController";
|
||||
import {adminApi} from "../Services/AdminApi";
|
||||
import {jwtTokenManager} from "../Services/JWTTokenManager";
|
||||
import {parse} from "query-string";
|
||||
|
||||
export interface TokenInterface {
|
||||
userUuid: string
|
||||
}
|
||||
|
||||
export class AuthenticateController extends BaseController {
|
||||
|
||||
constructor(private App : TemplatedApp) {
|
||||
super();
|
||||
this.register();
|
||||
this.verify();
|
||||
this.anonymLogin();
|
||||
}
|
||||
|
||||
//Try to login with an admin token
|
||||
private register(){
|
||||
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
this.App.post("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||
(async () => {
|
||||
res.onAborted(() => {
|
||||
console.warn('Login request was aborted');
|
||||
})
|
||||
const param = await res.json();
|
||||
|
||||
//todo: what to do if the organizationMemberToken is already used?
|
||||
const organizationMemberToken:string|null = param.organizationMemberToken;
|
||||
|
||||
try {
|
||||
if (typeof organizationMemberToken != 'string') throw new Error('No organization token');
|
||||
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
||||
const userUuid = data.userUuid;
|
||||
const organizationSlug = data.organizationSlug;
|
||||
const worldSlug = data.worldSlug;
|
||||
const roomSlug = data.roomSlug;
|
||||
const mapUrlStart = data.mapUrlStart;
|
||||
const textures = data.textures;
|
||||
|
||||
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
||||
res.writeStatus("200 OK");
|
||||
this.addCorsHeaders(res);
|
||||
res.end(JSON.stringify({
|
||||
authToken,
|
||||
userUuid,
|
||||
organizationSlug,
|
||||
worldSlug,
|
||||
roomSlug,
|
||||
mapUrlStart,
|
||||
textures
|
||||
}));
|
||||
|
||||
} catch (e) {
|
||||
console.error("An error happened", e)
|
||||
res.writeStatus(e.status || "500 Internal Server Error");
|
||||
this.addCorsHeaders(res);
|
||||
res.end('An error happened');
|
||||
}
|
||||
|
||||
|
||||
})();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private verify(){
|
||||
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => {
|
||||
(async () => {
|
||||
const query = parse(req.getQuery());
|
||||
|
||||
res.onAborted(() => {
|
||||
console.warn('verify request was aborted');
|
||||
})
|
||||
|
||||
try {
|
||||
await jwtTokenManager.getUserUuidFromToken(query.token as string);
|
||||
} catch (e) {
|
||||
res.writeStatus("400 Bad Request");
|
||||
this.addCorsHeaders(res);
|
||||
res.end(JSON.stringify({
|
||||
"success": false,
|
||||
"message": "Invalid JWT token"
|
||||
}));
|
||||
return;
|
||||
}
|
||||
res.writeStatus("200 OK");
|
||||
this.addCorsHeaders(res);
|
||||
res.end(JSON.stringify({
|
||||
"success": true
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//permit to login on application. Return token to connect on Websocket IO.
|
||||
private anonymLogin(){
|
||||
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||
|
||||
res.onAborted(() => {
|
||||
console.warn('Login request was aborted');
|
||||
})
|
||||
|
||||
const userUuid = v4();
|
||||
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
||||
res.writeStatus("200 OK");
|
||||
this.addCorsHeaders(res);
|
||||
res.end(JSON.stringify({
|
||||
authToken,
|
||||
userUuid,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import { HttpResponse } from "uWebSockets.js";
|
||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
||||
|
||||
|
||||
export class BaseController {
|
||||
protected addCorsHeaders(res: HttpResponse): void {
|
||||
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||
res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
|
||||
res.writeHeader("access-control-allow-origin", "*");
|
||||
res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
||||
res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
||||
res.writeHeader('access-control-allow-origin', '*');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,68 +1,45 @@
|
|||
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||
import { stringify } from "circular-json";
|
||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||
import { parse } from "query-string";
|
||||
import { App } from "../Server/sifrr.server";
|
||||
import { socketManager } from "../Services/SocketManager";
|
||||
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
||||
import {IoSocketController} from "_Controller/IoSocketController";
|
||||
import {stringify} from "circular-json";
|
||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||
import { parse } from 'query-string';
|
||||
import {App} from "../Server/sifrr.server";
|
||||
import {socketManager} from "../Services/SocketManager";
|
||||
|
||||
export class DebugController {
|
||||
constructor(private App: App) {
|
||||
constructor(private App : App) {
|
||||
this.getDump();
|
||||
}
|
||||
|
||||
getDump() {
|
||||
|
||||
getDump(){
|
||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
||||
(async () => {
|
||||
const query = parse(req.getQuery());
|
||||
const query = parse(req.getQuery());
|
||||
|
||||
if (ADMIN_API_TOKEN === "") {
|
||||
return res.writeStatus("401 Unauthorized").end("No token configured!");
|
||||
}
|
||||
if (query.token !== ADMIN_API_TOKEN) {
|
||||
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
||||
}
|
||||
if (query.token !== ADMIN_API_TOKEN) {
|
||||
return res.status(401).send('Invalid token sent!');
|
||||
}
|
||||
|
||||
return res
|
||||
.writeStatus("200 OK")
|
||||
.writeHeader("Content-Type", "application/json")
|
||||
.end(
|
||||
stringify(
|
||||
await Promise.all(socketManager.getWorlds().values()),
|
||||
(key: unknown, value: unknown) => {
|
||||
if (key === "listeners") {
|
||||
return "Listeners";
|
||||
}
|
||||
if (key === "socket") {
|
||||
return "Socket";
|
||||
}
|
||||
if (key === "batchedMessages") {
|
||||
return "BatchedMessages";
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
const obj: { [key: string | number]: unknown } = {};
|
||||
for (const [mapKey, mapValue] of value.entries()) {
|
||||
if (typeof mapKey === "number" || typeof mapKey === "string") {
|
||||
obj[mapKey] = mapValue;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
} else if (value instanceof Set) {
|
||||
const obj: Array<unknown> = [];
|
||||
for (const [setKey, setValue] of value.entries()) {
|
||||
obj.push(setValue);
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify(
|
||||
socketManager.getWorlds(),
|
||||
(key: unknown, value: unknown) => {
|
||||
if(value instanceof Map) {
|
||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
for (const [mapKey, mapValue] of value.entries()) {
|
||||
obj[mapKey] = mapValue;
|
||||
}
|
||||
return obj;
|
||||
} else if(value instanceof Set) {
|
||||
const obj: Array<unknown> = [];
|
||||
for (const [setKey, setValue] of value.entries()) {
|
||||
obj.push(setValue);
|
||||
}
|
||||
)
|
||||
);
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
res.writeStatus("500");
|
||||
res.end("An error occurred");
|
||||
});
|
||||
return obj;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ export class FileController extends BaseController {
|
|||
this.App.get("/download-audio-message/:id", (res: HttpResponse, req: HttpRequest) => {
|
||||
|
||||
res.onAborted(() => {
|
||||
console.warn('download-audio-message request was aborted');
|
||||
console.warn('upload-audio-message request was aborted');
|
||||
})
|
||||
|
||||
const id = req.getParameter(0);
|
330
back/src/Controller/IoSocketController.ts
Normal file
330
back/src/Controller/IoSocketController.ts
Normal file
|
@ -0,0 +1,330 @@
|
|||
import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||
import {GameRoomPolicyTypes} from "../Model/GameRoom";
|
||||
import {PointInterface} from "../Model/Websocket/PointInterface";
|
||||
import {
|
||||
SetPlayerDetailsMessage,
|
||||
SubMessage,
|
||||
BatchMessage,
|
||||
ItemEventMessage,
|
||||
ViewportMessage,
|
||||
ClientToServerMessage,
|
||||
SilentMessage,
|
||||
WebRtcSignalToServerMessage,
|
||||
PlayGlobalMessage,
|
||||
ReportPlayerMessage,
|
||||
QueryJitsiJwtMessage
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import {UserMovesMessage} from "../Messages/generated/messages_pb";
|
||||
import {TemplatedApp} from "uWebSockets.js"
|
||||
import {parse} from "query-string";
|
||||
import {jwtTokenManager} from "../Services/JWTTokenManager";
|
||||
import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi";
|
||||
import {SocketManager, socketManager} from "../Services/SocketManager";
|
||||
import {emitInBatch} from "../Services/IoSocketHelpers";
|
||||
import {clientEventsEmitter} from "../Services/ClientEventsEmitter";
|
||||
import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable";
|
||||
|
||||
export class IoSocketController {
|
||||
private nextUserId: number = 1;
|
||||
|
||||
constructor(private readonly app: TemplatedApp) {
|
||||
this.ioConnection();
|
||||
this.adminRoomSocket();
|
||||
}
|
||||
|
||||
adminRoomSocket() {
|
||||
this.app.ws('/admin/rooms', {
|
||||
upgrade: (res, req, context) => {
|
||||
const query = parse(req.getQuery());
|
||||
const websocketKey = req.getHeader('sec-websocket-key');
|
||||
const websocketProtocol = req.getHeader('sec-websocket-protocol');
|
||||
const websocketExtensions = req.getHeader('sec-websocket-extensions');
|
||||
const token = query.token;
|
||||
if (token !== ADMIN_API_TOKEN) {
|
||||
console.log('Admin access refused for token: '+token)
|
||||
res.writeStatus("401 Unauthorized").end('Incorrect token');
|
||||
}
|
||||
const roomId = query.roomId as string;
|
||||
|
||||
res.upgrade(
|
||||
{roomId},
|
||||
websocketKey, websocketProtocol, websocketExtensions, context,
|
||||
);
|
||||
},
|
||||
open: (ws) => {
|
||||
console.log('Admin socket connect for room: '+ws.roomId);
|
||||
ws.send('Data:'+JSON.stringify(socketManager.getAdminSocketDataFor(ws.roomId as string)));
|
||||
ws.clientJoinCallback = (clientUUid: string, roomId: string) => {
|
||||
const wsroomId = ws.roomId as string;
|
||||
if(wsroomId === roomId) {
|
||||
ws.send('MemberJoin:'+clientUUid+';'+roomId);
|
||||
}
|
||||
};
|
||||
ws.clientLeaveCallback = (clientUUid: string, roomId: string) => {
|
||||
const wsroomId = ws.roomId as string;
|
||||
if(wsroomId === roomId) {
|
||||
ws.send('MemberLeave:'+clientUUid+';'+roomId);
|
||||
}
|
||||
};
|
||||
clientEventsEmitter.registerToClientJoin(ws.clientJoinCallback);
|
||||
clientEventsEmitter.registerToClientLeave(ws.clientLeaveCallback);
|
||||
},
|
||||
message: (ws, arrayBuffer, isBinary): void => {
|
||||
try {
|
||||
//TODO refactor message type and data
|
||||
const message: {event: string, message: {type: string, message: unknown, userUuid: string}} =
|
||||
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
|
||||
|
||||
if(message.event === 'user-message') {
|
||||
const messageToEmit = (message.message as { message: string, type: string, userUuid: string });
|
||||
switch (message.message.type) {
|
||||
case 'ban': {
|
||||
socketManager.emitSendUserMessage(messageToEmit);
|
||||
break;
|
||||
}
|
||||
case 'banned': {
|
||||
const socketUser = socketManager.emitSendUserMessage(messageToEmit);
|
||||
setTimeout(() => {
|
||||
socketUser.close();
|
||||
}, 10000);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
close: (ws, code, message) => {
|
||||
//todo make sure this code unregister the right listeners
|
||||
clientEventsEmitter.unregisterFromClientJoin(ws.clientJoinCallback);
|
||||
clientEventsEmitter.unregisterFromClientLeave(ws.clientLeaveCallback);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ioConnection() {
|
||||
this.app.ws('/room', {
|
||||
/* Options */
|
||||
//compression: uWS.SHARED_COMPRESSOR,
|
||||
idleTimeout: SOCKET_IDLE_TIMER,
|
||||
maxPayloadLength: 16 * 1024 * 1024,
|
||||
maxBackpressure: 65536, // Maximum 64kB of data in the buffer.
|
||||
//idleTimeout: 10,
|
||||
upgrade: (res, req, context) => {
|
||||
//console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!');
|
||||
(async () => {
|
||||
/* Keep track of abortions */
|
||||
const upgradeAborted = {aborted: false};
|
||||
|
||||
res.onAborted(() => {
|
||||
/* We can simply signal that we were aborted */
|
||||
upgradeAborted.aborted = true;
|
||||
});
|
||||
|
||||
try {
|
||||
const url = req.getUrl();
|
||||
const query = parse(req.getQuery());
|
||||
const websocketKey = req.getHeader('sec-websocket-key');
|
||||
const websocketProtocol = req.getHeader('sec-websocket-protocol');
|
||||
const websocketExtensions = req.getHeader('sec-websocket-extensions');
|
||||
|
||||
const roomId = query.roomId;
|
||||
if (typeof roomId !== 'string') {
|
||||
throw new Error('Undefined room ID: ');
|
||||
}
|
||||
|
||||
const token = query.token;
|
||||
const x = Number(query.x);
|
||||
const y = Number(query.y);
|
||||
const top = Number(query.top);
|
||||
const bottom = Number(query.bottom);
|
||||
const left = Number(query.left);
|
||||
const right = Number(query.right);
|
||||
const name = query.name;
|
||||
if (typeof name !== 'string') {
|
||||
throw new Error('Expecting name');
|
||||
}
|
||||
if (name === '') {
|
||||
throw new Error('No empty name');
|
||||
}
|
||||
let characterLayers = query.characterLayers;
|
||||
if (characterLayers === null) {
|
||||
throw new Error('Expecting skin');
|
||||
}
|
||||
if (typeof characterLayers === 'string') {
|
||||
characterLayers = [ characterLayers ];
|
||||
}
|
||||
|
||||
const userUuid = await jwtTokenManager.getUserUuidFromToken(token);
|
||||
|
||||
let memberTags: string[] = [];
|
||||
let memberTextures: CharacterTexture[] = [];
|
||||
const room = await socketManager.getOrCreateRoom(roomId);
|
||||
if(room.isFull){
|
||||
throw new Error('Room is full');
|
||||
}
|
||||
if (ADMIN_API_URL) {
|
||||
try {
|
||||
const userData = await adminApi.fetchMemberDataByUuid(userUuid);
|
||||
//console.log('USERDATA', userData)
|
||||
memberTags = userData.tags;
|
||||
memberTextures = userData.textures;
|
||||
if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) {
|
||||
throw new Error('No correct tags')
|
||||
}
|
||||
//console.log('access granted for user '+userUuid+' and room '+roomId);
|
||||
} catch (e) {
|
||||
console.log('access not granted for user '+userUuid+' and room '+roomId);
|
||||
console.error(e);
|
||||
throw new Error('Client cannot acces this ressource.')
|
||||
}
|
||||
}
|
||||
|
||||
// Generate characterLayers objects from characterLayers string[]
|
||||
const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
|
||||
|
||||
if (upgradeAborted.aborted) {
|
||||
console.log("Ouch! Client disconnected before we could upgrade it!");
|
||||
/* You must not upgrade now */
|
||||
return;
|
||||
}
|
||||
|
||||
/* This immediately calls open handler, you must not use res after this call */
|
||||
res.upgrade({
|
||||
// Data passed here is accessible on the "websocket" socket object.
|
||||
url,
|
||||
token,
|
||||
userUuid,
|
||||
roomId,
|
||||
name,
|
||||
characterLayers: characterLayerObjs,
|
||||
tags: memberTags,
|
||||
textures: memberTextures,
|
||||
position: {
|
||||
x: x,
|
||||
y: y,
|
||||
direction: 'down',
|
||||
moving: false
|
||||
} as PointInterface,
|
||||
viewport: {
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
left
|
||||
}
|
||||
},
|
||||
/* Spell these correctly */
|
||||
websocketKey,
|
||||
websocketProtocol,
|
||||
websocketExtensions,
|
||||
context);
|
||||
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.log(e.message);
|
||||
res.writeStatus("401 Unauthorized").end(e.message);
|
||||
} else {
|
||||
console.log(e);
|
||||
res.writeStatus("500 Internal Server Error").end('An error occurred');
|
||||
}
|
||||
return;
|
||||
}
|
||||
})();
|
||||
},
|
||||
/* Handlers */
|
||||
open: (ws) => {
|
||||
// Let's join the room
|
||||
const client = this.initClient(ws); //todo: into the upgrade instead?
|
||||
socketManager.handleJoinRoom(client);
|
||||
|
||||
//get data information and show messages
|
||||
if (ADMIN_API_URL) {
|
||||
adminApi.fetchMemberDataByUuid(client.userUuid).then((res: FetchMemberDataByUuidResponse) => {
|
||||
if (!res.messages) {
|
||||
return;
|
||||
}
|
||||
res.messages.forEach((c: unknown) => {
|
||||
const messageToSend = c as { type: string, message: string };
|
||||
socketManager.emitSendUserMessage({
|
||||
userUuid: client.userUuid,
|
||||
type: messageToSend.type,
|
||||
message: messageToSend.message
|
||||
})
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error('fetchMemberDataByUuid => err', err);
|
||||
});
|
||||
}
|
||||
},
|
||||
message: (ws, arrayBuffer, isBinary): void => {
|
||||
const client = ws as ExSocketInterface;
|
||||
const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer));
|
||||
|
||||
if (message.hasViewportmessage()) {
|
||||
socketManager.handleViewport(client, message.getViewportmessage() as ViewportMessage);
|
||||
} else if (message.hasUsermovesmessage()) {
|
||||
socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
|
||||
} else if (message.hasSetplayerdetailsmessage()) {
|
||||
socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage);
|
||||
} else if (message.hasSilentmessage()) {
|
||||
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
|
||||
} else if (message.hasItemeventmessage()) {
|
||||
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
|
||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||
socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
|
||||
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||
socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
|
||||
} else if (message.hasPlayglobalmessage()) {
|
||||
socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage);
|
||||
} else if (message.hasReportplayermessage()){
|
||||
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
|
||||
} else if (message.hasQueryjitsijwtmessage()){
|
||||
socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
|
||||
}
|
||||
|
||||
/* Ok is false if backpressure was built up, wait for drain */
|
||||
//let ok = ws.send(message, isBinary);
|
||||
},
|
||||
drain: (ws) => {
|
||||
console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
|
||||
},
|
||||
close: (ws, code, message) => {
|
||||
const Client = (ws as ExSocketInterface);
|
||||
try {
|
||||
Client.disconnecting = true;
|
||||
//leave room
|
||||
socketManager.leaveRoom(Client);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "disconnect"');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private initClient(ws: any): ExSocketInterface {
|
||||
const client : ExSocketInterface = ws;
|
||||
client.userId = this.nextUserId;
|
||||
this.nextUserId++;
|
||||
client.userUuid = ws.userUuid;
|
||||
client.token = ws.token;
|
||||
client.batchedMessages = new BatchMessage();
|
||||
client.batchTimeout = null;
|
||||
client.emitInBatch = (payload: SubMessage): void => {
|
||||
emitInBatch(client, payload);
|
||||
}
|
||||
client.disconnecting = false;
|
||||
|
||||
client.name = ws.name;
|
||||
client.tags = ws.tags;
|
||||
client.textures = ws.textures;
|
||||
client.characterLayers = ws.characterLayers;
|
||||
client.roomId = ws.roomId;
|
||||
return client;
|
||||
}
|
||||
}
|
70
back/src/Controller/MapController.ts
Normal file
70
back/src/Controller/MapController.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import {OK} from "http-status-codes";
|
||||
import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable";
|
||||
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
|
||||
import {BaseController} from "./BaseController";
|
||||
import {parse} from "query-string";
|
||||
import {adminApi} from "../Services/AdminApi";
|
||||
|
||||
//todo: delete this
|
||||
export class MapController extends BaseController{
|
||||
|
||||
constructor(private App : TemplatedApp) {
|
||||
super();
|
||||
this.App = App;
|
||||
this.getMapUrl();
|
||||
}
|
||||
|
||||
|
||||
// Returns a map mapping map name to file name of the map
|
||||
getMapUrl() {
|
||||
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
|
||||
this.addCorsHeaders(res);
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
this.App.get("/map", (res: HttpResponse, req: HttpRequest) => {
|
||||
|
||||
res.onAborted(() => {
|
||||
console.warn('/map request was aborted');
|
||||
})
|
||||
|
||||
const query = parse(req.getQuery());
|
||||
|
||||
if (typeof query.organizationSlug !== 'string') {
|
||||
console.error('Expected organizationSlug parameter');
|
||||
res.writeStatus("400 Bad request");
|
||||
this.addCorsHeaders(res);
|
||||
res.end("Expected organizationSlug parameter");
|
||||
}
|
||||
if (typeof query.worldSlug !== 'string') {
|
||||
console.error('Expected worldSlug parameter');
|
||||
res.writeStatus("400 Bad request");
|
||||
this.addCorsHeaders(res);
|
||||
res.end("Expected worldSlug parameter");
|
||||
}
|
||||
if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) {
|
||||
console.error('Expected only one roomSlug parameter');
|
||||
res.writeStatus("400 Bad request");
|
||||
this.addCorsHeaders(res);
|
||||
res.end("Expected only one roomSlug parameter");
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined);
|
||||
|
||||
res.writeStatus("200 OK");
|
||||
this.addCorsHeaders(res);
|
||||
res.end(JSON.stringify(mapDetails));
|
||||
} catch (e) {
|
||||
console.error(e.message || e);
|
||||
res.writeStatus("500 Internal Server Error")
|
||||
this.addCorsHeaders(res);
|
||||
res.end("An error occurred");
|
||||
}
|
||||
})();
|
||||
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,23 +1,20 @@
|
|||
import { App } from "../Server/sifrr.server";
|
||||
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||
import { register, collectDefaultMetrics } from "prom-client";
|
||||
import {App} from "../Server/sifrr.server";
|
||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||
const register = require('prom-client').register;
|
||||
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
|
||||
|
||||
export class PrometheusController {
|
||||
constructor(private App: App) {
|
||||
collectDefaultMetrics({
|
||||
timeout: 10000,
|
||||
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
|
||||
});
|
||||
|
||||
this.App.get("/metrics", this.metrics.bind(this));
|
||||
this.App.get("/metrics.json", this.metricsAsJSON.bind(this));
|
||||
}
|
||||
|
||||
private metrics(res: HttpResponse, req: HttpRequest): void {
|
||||
res.writeHeader("Content-Type", register.contentType);
|
||||
res.writeHeader('Content-Type', register.contentType);
|
||||
res.end(register.metrics());
|
||||
}
|
||||
private metricsAsJSON(res: HttpResponse, req: HttpRequest): void {
|
||||
res.writeHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify(register.getMetricsAsJSON()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,28 @@
|
|||
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
||||
const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
||||
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
||||
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
|
||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
|
||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "";
|
||||
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
|
||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || '';
|
||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
|
||||
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600;
|
||||
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
||||
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
|
||||
const JITSI_ISS = process.env.JITSI_ISS || "";
|
||||
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
|
||||
const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080;
|
||||
const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
|
||||
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
||||
export const REDIS_HOST = process.env.REDIS_HOST || undefined;
|
||||
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
|
||||
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
|
||||
export const STORE_VARIABLES_FOR_LOCAL_MAPS = process.env.STORE_VARIABLES_FOR_LOCAL_MAPS === "true";
|
||||
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
||||
const JITSI_ISS = process.env.JITSI_ISS || '';
|
||||
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
|
||||
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
||||
|
||||
export {
|
||||
SECRET_KEY,
|
||||
URL_ROOM_STARTED,
|
||||
MINIMUM_DISTANCE,
|
||||
ADMIN_API_URL,
|
||||
ADMIN_API_TOKEN,
|
||||
HTTP_PORT,
|
||||
GRPC_PORT,
|
||||
MAX_USERS_PER_ROOM,
|
||||
GROUP_RADIUS,
|
||||
ALLOW_ARTILLERY,
|
||||
CPU_OVERHEAT_THRESHOLD,
|
||||
JITSI_URL,
|
||||
JITSI_ISS,
|
||||
SECRET_JITSI_KEY,
|
||||
};
|
||||
SECRET_JITSI_KEY
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
7
back/src/Model/Distance.ts
Normal file
7
back/src/Model/Distance.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {MessageUserPosition} from "../Model/Websocket/MessageUserPosition";
|
||||
|
||||
export interface Distance {
|
||||
distance: number,
|
||||
first: MessageUserPosition,
|
||||
second: MessageUserPosition,
|
||||
}
|
|
@ -1,226 +1,141 @@
|
|||
import { PointInterface } from "./Websocket/PointInterface";
|
||||
import { Group } from "./Group";
|
||||
import { User, UserSocket } from "./User";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import {
|
||||
EmoteCallback,
|
||||
EntersCallback,
|
||||
LeavesCallback,
|
||||
MovesCallback,
|
||||
PlayerDetailsUpdatedCallback,
|
||||
} from "_Model/Zone";
|
||||
import { PositionNotifier } from "./PositionNotifier";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import {
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmoteEventMessage,
|
||||
ErrorMessage,
|
||||
JoinRoomMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SubToPusherRoomMessage,
|
||||
VariableMessage,
|
||||
VariableWithTagMessage,
|
||||
ServerToClientMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||
import { Admin } from "../Model/Admin";
|
||||
import { adminApi } from "../Services/AdminApi";
|
||||
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
||||
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||
import { mapFetcher } from "../Services/MapFetcher";
|
||||
import { VariablesManager } from "../Services/VariablesManager";
|
||||
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import { LocalUrlError } from "../Services/LocalUrlError";
|
||||
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
||||
import { VariableError } from "../Services/VariableError";
|
||||
import { isRoomRedirect } from "../Services/AdminApi/RoomRedirect";
|
||||
import {PointInterface} from "./Websocket/PointInterface";
|
||||
import {Group} from "./Group";
|
||||
import {User} from "./User";
|
||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {Identificable} from "_Model/Websocket/Identificable";
|
||||
import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
|
||||
import {PositionNotifier} from "./PositionNotifier";
|
||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||
import {Movable} from "_Model/Movable";
|
||||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
|
||||
import {arrayIntersect} from "../Services/ArrayHelper";
|
||||
import {MAX_USERS_PER_ROOM} from "../Enum/EnvironmentVariable";
|
||||
|
||||
export type ConnectCallback = (user: User, group: Group) => void;
|
||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||
|
||||
export class GameRoom {
|
||||
// Users, sorted by ID
|
||||
private readonly users = new Map<number, User>();
|
||||
private readonly usersByUuid = new Map<string, User>();
|
||||
private readonly groups = new Set<Group>();
|
||||
private readonly admins = new Set<Admin>();
|
||||
export enum GameRoomPolicyTypes {
|
||||
ANONYMUS_POLICY = 1,
|
||||
MEMBERS_ONLY_POLICY,
|
||||
USE_TAGS_POLICY,
|
||||
}
|
||||
|
||||
private itemsState = new Map<number, unknown>();
|
||||
export class GameRoom {
|
||||
private readonly minDistance: number;
|
||||
private readonly groupRadius: number;
|
||||
|
||||
// Users, sorted by ID
|
||||
private readonly users: Map<number, User>;
|
||||
private readonly groups: Set<Group>;
|
||||
|
||||
private readonly connectCallback: ConnectCallback;
|
||||
private readonly disconnectCallback: DisconnectCallback;
|
||||
|
||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||
|
||||
private readonly positionNotifier: PositionNotifier;
|
||||
private versionNumber: number = 1;
|
||||
private nextUserId: number = 1;
|
||||
public readonly roomId: string;
|
||||
public readonly anonymous: boolean;
|
||||
public tags: string[];
|
||||
public policyType: GameRoomPolicyTypes;
|
||||
public readonly roomSlug: string;
|
||||
public readonly worldSlug: string = '';
|
||||
public readonly organizationSlug: string = '';
|
||||
|
||||
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
||||
constructor(roomId: string,
|
||||
connectCallback: ConnectCallback,
|
||||
disconnectCallback: DisconnectCallback,
|
||||
minDistance: number,
|
||||
groupRadius: number,
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback)
|
||||
{
|
||||
this.roomId = roomId;
|
||||
this.anonymous = isRoomAnonymous(roomId);
|
||||
this.tags = [];
|
||||
this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY;
|
||||
|
||||
private constructor(
|
||||
public readonly roomUrl: string,
|
||||
private mapUrl: string,
|
||||
private readonly connectCallback: ConnectCallback,
|
||||
private readonly disconnectCallback: DisconnectCallback,
|
||||
private readonly minDistance: number,
|
||||
private readonly groupRadius: number,
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback,
|
||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
) {
|
||||
if (this.anonymous) {
|
||||
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
||||
} else {
|
||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
|
||||
this.roomSlug = roomSlug;
|
||||
this.organizationSlug = organizationSlug;
|
||||
this.worldSlug = worldSlug;
|
||||
}
|
||||
|
||||
|
||||
this.users = new Map<number, User>();
|
||||
this.groups = new Set<Group>();
|
||||
this.connectCallback = connectCallback;
|
||||
this.disconnectCallback = disconnectCallback;
|
||||
this.minDistance = minDistance;
|
||||
this.groupRadius = groupRadius;
|
||||
// A zone is 10 sprites wide.
|
||||
this.positionNotifier = new PositionNotifier(
|
||||
320,
|
||||
320,
|
||||
onEnters,
|
||||
onMoves,
|
||||
onLeaves,
|
||||
onEmote,
|
||||
onPlayerDetailsUpdated
|
||||
);
|
||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves);
|
||||
}
|
||||
|
||||
public static async create(
|
||||
roomUrl: string,
|
||||
connectCallback: ConnectCallback,
|
||||
disconnectCallback: DisconnectCallback,
|
||||
minDistance: number,
|
||||
groupRadius: number,
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback,
|
||||
onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
): Promise<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 getGroups(): Group[] {
|
||||
return Array.from(this.groups.values());
|
||||
}
|
||||
|
||||
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);
|
||||
public join(socket : ExSocketInterface, userPosition: PointInterface): void {
|
||||
const user = new User(socket.userId, socket.userUuid, userPosition, false, this.positionNotifier, socket);
|
||||
this.users.set(socket.userId, user);
|
||||
// Let's call update position to trigger the join / leave room
|
||||
//this.updatePosition(socket, userPosition);
|
||||
this.updateUserGroup(user);
|
||||
|
||||
// Notify admins
|
||||
for (const admin of this.admins) {
|
||||
admin.sendUserJoin(user.uuid, user.name, user.IPAddress);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public leave(user: User) {
|
||||
const userObj = this.users.get(user.id);
|
||||
public leave(user : Identificable){
|
||||
const userObj = this.users.get(user.userId);
|
||||
if (userObj === undefined) {
|
||||
console.warn("User ", user.id, "does not belong to this game room! It should!");
|
||||
console.warn('User ', user.userId, 'does not belong to world! It should!');
|
||||
}
|
||||
if (userObj !== undefined && typeof userObj.group !== "undefined") {
|
||||
if (userObj !== undefined && typeof userObj.group !== 'undefined') {
|
||||
this.leaveGroup(userObj);
|
||||
}
|
||||
|
||||
if (user.hasFollowers()) {
|
||||
user.stopLeading();
|
||||
}
|
||||
if (user.following) {
|
||||
user.following.delFollower(user);
|
||||
}
|
||||
|
||||
this.users.delete(user.id);
|
||||
this.usersByUuid.delete(user.uuid);
|
||||
this.users.delete(user.userId);
|
||||
|
||||
if (userObj !== undefined) {
|
||||
this.positionNotifier.removeViewport(userObj);
|
||||
this.positionNotifier.leave(userObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify admins
|
||||
for (const admin of this.admins) {
|
||||
admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/);
|
||||
}
|
||||
get isFull(): boolean {
|
||||
return this.users.size >= MAX_USERS_PER_ROOM;
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.users.size === 0 && this.admins.size === 0;
|
||||
return this.users.size === 0;
|
||||
}
|
||||
|
||||
public updatePosition(user: User, userPosition: PointInterface): void {
|
||||
public updatePosition(socket : Identificable, userPosition: PointInterface): void {
|
||||
const user = this.users.get(socket.userId);
|
||||
if(typeof user === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
user.setPosition(userPosition);
|
||||
|
||||
this.updateUserGroup(user);
|
||||
}
|
||||
|
||||
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
|
||||
if (playerDetailsMessage.getRemoveoutlinecolor()) {
|
||||
user.outlineColor = undefined;
|
||||
} else {
|
||||
user.outlineColor = playerDetailsMessage.getOutlinecolor();
|
||||
}
|
||||
}
|
||||
|
||||
private updateUserGroup(user: User): void {
|
||||
user.group?.updatePosition();
|
||||
|
||||
if (user.silent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = user.group;
|
||||
const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user);
|
||||
|
||||
if (group === undefined) {
|
||||
if (user.group === undefined) {
|
||||
// If the user is not part of a group:
|
||||
// should he join a group?
|
||||
|
||||
|
@ -229,125 +144,38 @@ export class GameRoom {
|
|||
return;
|
||||
}
|
||||
|
||||
const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user);
|
||||
|
||||
if (closestItem !== null) {
|
||||
if (closestItem instanceof Group) {
|
||||
// Let's join the group!
|
||||
closestItem.join(user);
|
||||
closestItem.setOutOfBounds(false);
|
||||
} else {
|
||||
const closestUser: User = closestItem;
|
||||
const group: Group = new Group(
|
||||
this.roomUrl,
|
||||
[user, closestUser],
|
||||
this.groupRadius,
|
||||
this.connectCallback,
|
||||
this.disconnectCallback,
|
||||
this.positionNotifier
|
||||
);
|
||||
const closestUser : User = closestItem;
|
||||
const group: Group = new Group(this.roomId,[
|
||||
user,
|
||||
closestUser
|
||||
], this.connectCallback, this.disconnectCallback, this.positionNotifier);
|
||||
this.groups.add(group);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
let hasKickOutSomeone = false;
|
||||
let followingMembers: User[] = [];
|
||||
|
||||
const previewNewGroupPosition = group.previewGroupPosition();
|
||||
|
||||
if (!previewNewGroupPosition) {
|
||||
// If the user is part of a group:
|
||||
// should he leave the group?
|
||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition());
|
||||
if (distance > this.groupRadius) {
|
||||
this.leaveGroup(user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.hasFollowers() || user.following) {
|
||||
followingMembers = user.hasFollowers()
|
||||
? group.getUsers().filter((currentUser) => currentUser.following === user)
|
||||
: group.getUsers().filter((currentUser) => currentUser.following === user.following);
|
||||
|
||||
// If all group members are part of the same follow group
|
||||
if (group.getUsers().length - 1 === followingMembers.length) {
|
||||
let isOutOfBounds = false;
|
||||
|
||||
// If a follower is far away from the leader, "outOfBounds" is set to true
|
||||
for (const member of followingMembers) {
|
||||
const distance = GameRoom.computeDistanceBetweenPositions(
|
||||
member.getPosition(),
|
||||
previewNewGroupPosition
|
||||
);
|
||||
|
||||
if (distance > this.groupRadius) {
|
||||
isOutOfBounds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
group.setOutOfBounds(isOutOfBounds);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the moving user has kicked out another user
|
||||
for (const headMember of group.getGroupHeads()) {
|
||||
if (!headMember.group) {
|
||||
this.leaveGroup(headMember);
|
||||
continue;
|
||||
}
|
||||
|
||||
const headPosition = headMember.getPosition();
|
||||
const distance = GameRoom.computeDistanceBetweenPositions(headPosition, previewNewGroupPosition);
|
||||
|
||||
if (distance > this.groupRadius) {
|
||||
hasKickOutSomeone = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the current moving user has kicked another user from the radius,
|
||||
* the moving user leaves the group because he is too far away.
|
||||
*/
|
||||
const userDistance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), previewNewGroupPosition);
|
||||
|
||||
if (hasKickOutSomeone && userDistance > this.groupRadius) {
|
||||
if (user.hasFollowers() && group.getUsers().length === 3 && followingMembers.length === 1) {
|
||||
const other = group
|
||||
.getUsers()
|
||||
.find((currentUser) => !currentUser.hasFollowers() && !currentUser.following);
|
||||
if (other) {
|
||||
this.leaveGroup(other);
|
||||
}
|
||||
} else if (user.hasFollowers()) {
|
||||
this.leaveGroup(user);
|
||||
for (const member of followingMembers) {
|
||||
this.leaveGroup(member);
|
||||
}
|
||||
|
||||
// Re-create a group with the followers
|
||||
const newGroup: Group = new Group(
|
||||
this.roomUrl,
|
||||
[user, ...followingMembers],
|
||||
this.groupRadius,
|
||||
this.connectCallback,
|
||||
this.disconnectCallback,
|
||||
this.positionNotifier
|
||||
);
|
||||
this.groups.add(newGroup);
|
||||
} else {
|
||||
this.leaveGroup(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user.group?.updatePosition();
|
||||
user.group?.searchForNearbyUsers();
|
||||
}
|
||||
|
||||
public sendToOthersInGroupIncludingUser(user: User, message: ServerToClientMessage): void {
|
||||
user.group?.getUsers().forEach((currentUser: User) => {
|
||||
if (currentUser.id !== user.id) {
|
||||
currentUser.socket.write(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setSilent(user: User, silent: boolean) {
|
||||
setSilent(socket: Identificable, silent: boolean) {
|
||||
const user = this.users.get(socket.userId);
|
||||
if(typeof user === 'undefined') {
|
||||
console.warn('In setSilent, could not find user with ID "'+socket.userId+'" in world.');
|
||||
return;
|
||||
}
|
||||
if (user.silent === silent) {
|
||||
return;
|
||||
}
|
||||
|
@ -358,7 +186,7 @@ export class GameRoom {
|
|||
}
|
||||
if (!silent) {
|
||||
// If we are back to life, let's trigger a position update to see if we can join some group.
|
||||
this.updatePosition(user, user.getPosition());
|
||||
this.updatePosition(socket, user.getPosition());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -374,9 +202,10 @@ export class GameRoom {
|
|||
}
|
||||
group.leave(user);
|
||||
if (group.isEmpty()) {
|
||||
this.positionNotifier.leave(group);
|
||||
group.destroy();
|
||||
if (!this.groups.has(group)) {
|
||||
throw new Error(`Could not find group ${group.getId()} referenced by user ${user.id} in World.`);
|
||||
throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World.");
|
||||
}
|
||||
this.groups.delete(group);
|
||||
//todo: is the group garbage collected?
|
||||
|
@ -394,15 +223,16 @@ export class GameRoom {
|
|||
* OR
|
||||
* - close enough to a group (distance <= groupRadius)
|
||||
*/
|
||||
private searchClosestAvailableUserOrGroup(user: User): User | Group | null {
|
||||
private searchClosestAvailableUserOrGroup(user: User): User|Group|null
|
||||
{
|
||||
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
||||
let matchingItem: User | Group | null = null;
|
||||
this.users.forEach((currentUser, userId) => {
|
||||
// Let's only check users that are not part of a group
|
||||
if (typeof currentUser.group !== "undefined") {
|
||||
if (typeof currentUser.group !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (currentUser === user) {
|
||||
if(currentUser === user) {
|
||||
return;
|
||||
}
|
||||
if (currentUser.silent) {
|
||||
|
@ -411,7 +241,7 @@ export class GameRoom {
|
|||
|
||||
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
||||
|
||||
if (distance <= minimumDistanceFound && distance <= this.minDistance) {
|
||||
if(distance <= minimumDistanceFound && distance <= this.minDistance) {
|
||||
minimumDistanceFound = distance;
|
||||
matchingItem = currentUser;
|
||||
}
|
||||
|
@ -422,7 +252,7 @@ export class GameRoom {
|
|||
return;
|
||||
}
|
||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
||||
if (distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
||||
if(distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
||||
minimumDistanceFound = distance;
|
||||
matchingItem = group;
|
||||
}
|
||||
|
@ -431,15 +261,15 @@ export class GameRoom {
|
|||
return matchingItem;
|
||||
}
|
||||
|
||||
public static computeDistance(user1: User, user2: User): number {
|
||||
public static computeDistance(user1: User, user2: User): number
|
||||
{
|
||||
const user1Position = user1.getPosition();
|
||||
const user2Position = user2.getPosition();
|
||||
return Math.sqrt(
|
||||
Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)
|
||||
);
|
||||
return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2));
|
||||
}
|
||||
|
||||
public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number {
|
||||
public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number
|
||||
{
|
||||
return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2));
|
||||
}
|
||||
|
||||
|
@ -451,207 +281,17 @@ export class GameRoom {
|
|||
return this.itemsState;
|
||||
}
|
||||
|
||||
public async setVariable(name: string, value: string, user: User): Promise<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;
|
||||
}
|
||||
setViewport(socket : Identificable, viewport: ViewportInterface): Movable[] {
|
||||
const user = this.users.get(socket.userId);
|
||||
if(typeof user === 'undefined') {
|
||||
console.warn('In setViewport, could not find user with ID "'+socket.userId+'" in world.');
|
||||
return [];
|
||||
}
|
||||
return this.positionNotifier.setViewport(user, viewport);
|
||||
}
|
||||
|
||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<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);
|
||||
canAccess(userTags: string[]): boolean {
|
||||
return arrayIntersect(userTags, this.tags);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,36 @@
|
|||
import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom";
|
||||
import { ConnectCallback, DisconnectCallback } from "./GameRoom";
|
||||
import { User } from "./User";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionNotifier } from "_Model/PositionNotifier";
|
||||
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
|
||||
import type { Zone } from "../Model/Zone";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {Movable} from "_Model/Movable";
|
||||
import {PositionNotifier} from "_Model/PositionNotifier";
|
||||
import {gaugeManager} from "../Services/GaugeManager";
|
||||
|
||||
export class Group implements Movable {
|
||||
static readonly MAX_PER_GROUP = 4;
|
||||
|
||||
private static nextId: number = 1;
|
||||
|
||||
private id: number;
|
||||
private users: Set<User>;
|
||||
private x!: number;
|
||||
private y!: number;
|
||||
private hasEditedGauge: boolean = false;
|
||||
private wasDestroyed: boolean = false;
|
||||
private roomId: string;
|
||||
private currentZone: Zone | null = null;
|
||||
/**
|
||||
* When outOfBounds = true, a user if out of the bounds of the group BUT still considered inside it (because we are in following mode)
|
||||
*/
|
||||
private outOfBounds = false;
|
||||
|
||||
constructor(
|
||||
roomId: string,
|
||||
users: User[],
|
||||
private groupRadius: number,
|
||||
private connectCallback: ConnectCallback,
|
||||
private disconnectCallback: DisconnectCallback,
|
||||
private positionNotifier: PositionNotifier
|
||||
) {
|
||||
|
||||
constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) {
|
||||
this.roomId = roomId;
|
||||
this.users = new Set<User>();
|
||||
this.id = Group.nextId;
|
||||
Group.nextId++;
|
||||
//we only send a event for prometheus metrics if the group lives more than 5 seconds
|
||||
setTimeout(() => {
|
||||
if (!this.wasDestroyed) {
|
||||
this.hasEditedGauge = true;
|
||||
gaugeManager.incNbGroupsPerRoomGauge(roomId);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
users.forEach((user: User) => {
|
||||
this.join(user);
|
||||
|
@ -45,7 +43,7 @@ export class Group implements Movable {
|
|||
return Array.from(this.users.values());
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
getId() : number {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
|
@ -55,43 +53,10 @@ export class Group implements Movable {
|
|||
getPosition(): PositionInterface {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
y: this.y
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of users of the group, ignoring any "followers".
|
||||
* Useful to compute the position of the group if a follower is "trapped" far away from the the leader.
|
||||
*/
|
||||
getGroupHeads(): User[] {
|
||||
return Array.from(this.users).filter((user) => user.group?.leader === user || !user.following);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the position of the group but don't update it
|
||||
*/
|
||||
previewGroupPosition(): { x: number; y: number } | undefined {
|
||||
const users = this.getGroupHeads();
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
if (users.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
users.forEach((user: User) => {
|
||||
const position = user.getPosition();
|
||||
x += position.x;
|
||||
y += position.y;
|
||||
});
|
||||
|
||||
x /= users.length;
|
||||
y /= users.length;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the barycenter of all users (i.e. the center of the group)
|
||||
*/
|
||||
|
@ -99,63 +64,50 @@ export class Group implements Movable {
|
|||
const oldX = this.x;
|
||||
const oldY = this.y;
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
// Let's compute the barycenter of all users.
|
||||
const newPosition = this.previewGroupPosition();
|
||||
|
||||
if (!newPosition) {
|
||||
return;
|
||||
this.users.forEach((user: User) => {
|
||||
const position = user.getPosition();
|
||||
x += position.x;
|
||||
y += position.y;
|
||||
});
|
||||
x /= this.users.size;
|
||||
y /= this.users.size;
|
||||
if (this.users.size === 0) {
|
||||
throw new Error("EMPTY GROUP FOUND!!!");
|
||||
}
|
||||
|
||||
const { x, y } = newPosition;
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
if (this.outOfBounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldX === undefined) {
|
||||
this.currentZone = this.positionNotifier.enter(this);
|
||||
this.positionNotifier.enter(this);
|
||||
} else {
|
||||
this.currentZone = this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY });
|
||||
}
|
||||
}
|
||||
|
||||
searchForNearbyUsers(): void {
|
||||
if (!this.currentZone) return;
|
||||
|
||||
for (const user of this.positionNotifier.getAllUsersInSquareAroundZone(this.currentZone)) {
|
||||
// Todo: Merge two groups with a leader
|
||||
if (user.group || this.isFull()) return; //we ignore users that are already in a group.
|
||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), this.getPosition());
|
||||
if (distance < this.groupRadius) {
|
||||
this.join(user);
|
||||
this.setOutOfBounds(false);
|
||||
this.updatePosition();
|
||||
}
|
||||
this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY});
|
||||
}
|
||||
}
|
||||
|
||||
isFull(): boolean {
|
||||
return this.users.size >= MAX_PER_GROUP;
|
||||
return this.users.size >= Group.MAX_PER_GROUP;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.users.size <= 1;
|
||||
}
|
||||
|
||||
join(user: User): void {
|
||||
join(user: User): void
|
||||
{
|
||||
// Broadcast on the right event
|
||||
this.connectCallback(user, this);
|
||||
this.users.add(user);
|
||||
user.group = this;
|
||||
}
|
||||
|
||||
leave(user: User): void {
|
||||
leave(user: User): void
|
||||
{
|
||||
const success = this.users.delete(user);
|
||||
if (success === false) {
|
||||
throw new Error(`Could not find user ${user.id} in the group ${this.id}`);
|
||||
throw new Error("Could not find user "+user.id+" in the group "+this.id);
|
||||
}
|
||||
user.group = undefined;
|
||||
|
||||
|
@ -171,44 +123,16 @@ export class Group implements Movable {
|
|||
* Let's kick everybody out.
|
||||
* Usually used when there is only one user left.
|
||||
*/
|
||||
destroy(): void {
|
||||
if (!this.outOfBounds) {
|
||||
this.positionNotifier.leave(this);
|
||||
}
|
||||
|
||||
destroy(): void
|
||||
{
|
||||
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
|
||||
for (const user of this.users) {
|
||||
this.leave(user);
|
||||
}
|
||||
this.wasDestroyed = true;
|
||||
}
|
||||
|
||||
get getSize() {
|
||||
get getSize(){
|
||||
return this.users.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* A group can have at most one person leading the way in it.
|
||||
*/
|
||||
get leader(): User | undefined {
|
||||
for (const user of this.users) {
|
||||
if (user.hasFollowers()) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setOutOfBounds(outOfBounds: boolean): void {
|
||||
if (this.outOfBounds === true && outOfBounds === false) {
|
||||
this.positionNotifier.enter(this);
|
||||
this.outOfBounds = false;
|
||||
} else if (this.outOfBounds === false && outOfBounds === true) {
|
||||
this.positionNotifier.leave(this);
|
||||
this.outOfBounds = true;
|
||||
}
|
||||
}
|
||||
|
||||
get getOutOfBounds() {
|
||||
return this.outOfBounds;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
|
||||
/**
|
||||
* A physical object that can be placed into a Zone
|
||||
*/
|
||||
export interface Movable {
|
||||
getPosition(): PositionInterface;
|
||||
getPosition(): PositionInterface
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export interface PositionInterface {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number,
|
||||
y: number
|
||||
}
|
||||
|
|
|
@ -8,67 +8,80 @@
|
|||
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
||||
* number of players around the current player.
|
||||
*/
|
||||
import {
|
||||
EmoteCallback,
|
||||
EntersCallback,
|
||||
LeavesCallback,
|
||||
MovesCallback,
|
||||
PlayerDetailsUpdatedCallback,
|
||||
Zone,
|
||||
} from "./Zone";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { ZoneSocket } from "../RoomManager";
|
||||
import { User } from "../Model/User";
|
||||
import { EmoteEventMessage, SetPlayerDetailsMessage } from "../Messages/generated/messages_pb";
|
||||
import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
|
||||
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||
import {User} from "_Model/User";
|
||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||
import {Movable} from "_Model/Movable";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
|
||||
interface ZoneDescriptor {
|
||||
i: number;
|
||||
j: number;
|
||||
}
|
||||
|
||||
export function* getNearbyDescriptorsMatrix(middleZoneDescriptor: ZoneDescriptor): Generator<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!)
|
||||
|
||||
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
||||
|
||||
private zones: Zone[][] = [];
|
||||
|
||||
constructor(
|
||||
private zoneWidth: number,
|
||||
private zoneHeight: number,
|
||||
private onUserEnters: EntersCallback,
|
||||
private onUserMoves: MovesCallback,
|
||||
private onUserLeaves: LeavesCallback,
|
||||
private onEmote: EmoteCallback,
|
||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback
|
||||
) {}
|
||||
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback) {
|
||||
}
|
||||
|
||||
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||
return {
|
||||
i: Math.floor(x / this.zoneWidth),
|
||||
j: Math.floor(y / this.zoneHeight),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enter(thing: Movable): Zone {
|
||||
/**
|
||||
* Sets the viewport coordinates.
|
||||
* Returns the list of new users to add
|
||||
*/
|
||||
public setViewport(user: User, viewport: ViewportInterface): Movable[] {
|
||||
if (viewport.left > viewport.right || viewport.top > viewport.bottom) {
|
||||
console.warn('Invalid viewport received: ', viewport);
|
||||
return [];
|
||||
}
|
||||
|
||||
const oldZones = user.listenedZones;
|
||||
const newZones = new Set<Zone>();
|
||||
|
||||
const topLeftDesc = this.getZoneDescriptorFromCoordinates(viewport.left, viewport.top);
|
||||
const bottomRightDesc = this.getZoneDescriptorFromCoordinates(viewport.right, viewport.bottom);
|
||||
|
||||
for (let j = topLeftDesc.j; j <= bottomRightDesc.j; j++) {
|
||||
for (let i = topLeftDesc.i; i <= bottomRightDesc.i; i++) {
|
||||
newZones.add(this.getZone(i, j));
|
||||
}
|
||||
}
|
||||
|
||||
const addedZones = [...newZones].filter(x => !oldZones.has(x));
|
||||
const removedZones = [...oldZones].filter(x => !newZones.has(x));
|
||||
|
||||
|
||||
let things: Movable[] = [];
|
||||
for (const zone of addedZones) {
|
||||
zone.startListening(user);
|
||||
things = things.concat(Array.from(zone.getThings()))
|
||||
}
|
||||
for (const zone of removedZones) {
|
||||
zone.stopListening(user);
|
||||
}
|
||||
|
||||
return things;
|
||||
}
|
||||
|
||||
public enter(thing: Movable): void {
|
||||
const position = thing.getPosition();
|
||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
||||
zone.enter(thing, null, position);
|
||||
return zone;
|
||||
}
|
||||
|
||||
public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): Zone {
|
||||
public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): void {
|
||||
// Did we change zone?
|
||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
||||
const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y);
|
||||
|
@ -82,11 +95,9 @@ export class PositionNotifier {
|
|||
|
||||
// Enter new zone
|
||||
newZone.enter(thing, oldZone, newPosition);
|
||||
return newZone;
|
||||
} else {
|
||||
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||
zone.move(thing, newPosition);
|
||||
return zone;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,6 +108,13 @@ export class PositionNotifier {
|
|||
oldZone.leave(thing, null);
|
||||
}
|
||||
|
||||
public removeViewport(user: User): void {
|
||||
// Also, let's stop listening on viewports
|
||||
for (const zone of user.listenedZones) {
|
||||
zone.stopListening(user);
|
||||
}
|
||||
}
|
||||
|
||||
private getZone(i: number, j: number): Zone {
|
||||
let zoneRow = this.zones[j];
|
||||
if (zoneRow === undefined) {
|
||||
|
@ -106,53 +124,9 @@ export class PositionNotifier {
|
|||
|
||||
let zone = this.zones[j][i];
|
||||
if (zone === undefined) {
|
||||
zone = new Zone(
|
||||
this.onUserEnters,
|
||||
this.onUserMoves,
|
||||
this.onUserLeaves,
|
||||
this.onEmote,
|
||||
this.onPlayerDetailsUpdated,
|
||||
i,
|
||||
j
|
||||
);
|
||||
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, i, j);
|
||||
this.zones[j][i] = zone;
|
||||
}
|
||||
return zone;
|
||||
}
|
||||
|
||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<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);
|
||||
}
|
||||
}
|
||||
|
|
30
back/src/Model/RoomIdentifier.ts
Normal file
30
back/src/Model/RoomIdentifier.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
//helper functions to parse room IDs
|
||||
|
||||
export const isRoomAnonymous = (roomID: string): boolean => {
|
||||
if (roomID.startsWith('_/')) {
|
||||
return true;
|
||||
} else if(roomID.startsWith('@/')) {
|
||||
return false;
|
||||
} else {
|
||||
throw new Error('Incorrect room ID: '+roomID);
|
||||
}
|
||||
}
|
||||
|
||||
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
||||
const idParts = roomId.split('/');
|
||||
if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId);
|
||||
return idParts.slice(2).join('/');
|
||||
}
|
||||
export interface extractDataFromPrivateRoomIdResponse {
|
||||
organizationSlug: string;
|
||||
worldSlug: string;
|
||||
roomSlug: string;
|
||||
}
|
||||
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
||||
const idParts = roomId.split('/');
|
||||
if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId);
|
||||
const organizationSlug = idParts[1];
|
||||
const worldSlug = idParts[2];
|
||||
const roomSlug = idParts[3];
|
||||
return {organizationSlug, worldSlug, roomSlug}
|
||||
}
|
|
@ -1,43 +1,22 @@
|
|||
import { Group } from "./Group";
|
||||
import { PointInterface } from "./Websocket/PointInterface";
|
||||
import { Zone } from "_Model/Zone";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionNotifier } from "_Model/PositionNotifier";
|
||||
import { ServerDuplexStream } from "grpc";
|
||||
import {
|
||||
BatchMessage,
|
||||
CompanionMessage,
|
||||
FollowAbortMessage,
|
||||
FollowConfirmationMessage,
|
||||
PusherToBackMessage,
|
||||
ServerToClientMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
SubMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
||||
|
||||
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
||||
import {Zone} from "_Model/Zone";
|
||||
import {Movable} from "_Model/Movable";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {PositionNotifier} from "_Model/PositionNotifier";
|
||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||
|
||||
export class User implements Movable {
|
||||
public listenedZones: Set<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,
|
||||
public uuid: string,
|
||||
private position: PointInterface,
|
||||
public silent: boolean,
|
||||
private positionNotifier: PositionNotifier,
|
||||
public readonly socket: UserSocket,
|
||||
public readonly tags: string[],
|
||||
public readonly visitCardUrl: string | null,
|
||||
public readonly name: string,
|
||||
public readonly characterLayers: CharacterLayer[],
|
||||
public readonly companion?: CompanionMessage,
|
||||
private _outlineColor?: number | undefined
|
||||
public readonly socket: ExSocketInterface
|
||||
) {
|
||||
this.listenedZones = new Set<Zone>();
|
||||
|
||||
|
@ -53,78 +32,4 @@ export class User implements Movable {
|
|||
this.position = position;
|
||||
this.positionNotifier.updatePosition(this, position, oldPosition);
|
||||
}
|
||||
|
||||
public addFollower(follower: User): void {
|
||||
this.followedBy.add(follower);
|
||||
follower._following = this;
|
||||
|
||||
const message = new FollowConfirmationMessage();
|
||||
message.setFollower(follower.id);
|
||||
message.setLeader(this.id);
|
||||
const clientMessage = new ServerToClientMessage();
|
||||
clientMessage.setFollowconfirmationmessage(message);
|
||||
this.socket.write(clientMessage);
|
||||
}
|
||||
|
||||
public delFollower(follower: User): void {
|
||||
this.followedBy.delete(follower);
|
||||
follower._following = undefined;
|
||||
|
||||
const message = new FollowAbortMessage();
|
||||
message.setFollower(follower.id);
|
||||
message.setLeader(this.id);
|
||||
const clientMessage = new ServerToClientMessage();
|
||||
clientMessage.setFollowabortmessage(message);
|
||||
this.socket.write(clientMessage);
|
||||
follower.socket.write(clientMessage);
|
||||
}
|
||||
|
||||
public hasFollowers(): boolean {
|
||||
return this.followedBy.size !== 0;
|
||||
}
|
||||
|
||||
get following(): User | undefined {
|
||||
return this._following;
|
||||
}
|
||||
|
||||
public stopLeading(): void {
|
||||
for (const follower of this.followedBy) {
|
||||
this.delFollower(follower);
|
||||
}
|
||||
}
|
||||
|
||||
private batchedMessages: BatchMessage = new BatchMessage();
|
||||
private batchTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
public emitInBatch(payload: SubMessage): void {
|
||||
this.batchedMessages.addPayload(payload);
|
||||
|
||||
if (this.batchTimeout === null) {
|
||||
this.batchTimeout = setTimeout(() => {
|
||||
/*if (socket.disconnecting) {
|
||||
return;
|
||||
}*/
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setBatchmessage(this.batchedMessages);
|
||||
|
||||
this.socket.write(serverToClientMessage);
|
||||
this.batchedMessages = new BatchMessage();
|
||||
this.batchTimeout = null;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
public set outlineColor(value: number | undefined) {
|
||||
this._outlineColor = value;
|
||||
|
||||
const playerDetails = new SetPlayerDetailsMessage();
|
||||
if (value === undefined) {
|
||||
playerDetails.setRemoveoutlinecolor(true);
|
||||
} else {
|
||||
playerDetails.setOutlinecolor(value);
|
||||
}
|
||||
|
||||
this.positionNotifier.updatePlayerDetails(this, playerDetails);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export interface CharacterLayer {
|
||||
name: string;
|
||||
url: string | undefined;
|
||||
}
|
31
back/src/Model/Websocket/ExSocketInterface.ts
Normal file
31
back/src/Model/Websocket/ExSocketInterface.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {PointInterface} from "./PointInterface";
|
||||
import {Identificable} from "./Identificable";
|
||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||
import {BatchMessage, SubMessage} from "../../Messages/generated/messages_pb";
|
||||
import {WebSocket} from "uWebSockets.js"
|
||||
import {CharacterTexture} from "../../Services/AdminApi";
|
||||
|
||||
export interface CharacterLayer {
|
||||
name: string,
|
||||
url: string|undefined
|
||||
}
|
||||
|
||||
export interface ExSocketInterface extends WebSocket, Identificable {
|
||||
token: string;
|
||||
roomId: string;
|
||||
//userId: number; // A temporary (autoincremented) identifier for this user
|
||||
userUuid: string; // A unique identifier for this user
|
||||
name: string;
|
||||
characterLayers: CharacterLayer[];
|
||||
position: PointInterface;
|
||||
viewport: ViewportInterface;
|
||||
/**
|
||||
* Pushes an event that will be sent in the next batch of events
|
||||
*/
|
||||
emitInBatch: (payload: SubMessage) => void;
|
||||
batchedMessages: BatchMessage;
|
||||
batchTimeout: NodeJS.Timeout|null;
|
||||
disconnecting: boolean,
|
||||
tags: string[],
|
||||
textures: CharacterTexture[],
|
||||
}
|
6
back/src/Model/Websocket/GroupUpdateInterface.ts
Normal file
6
back/src/Model/Websocket/GroupUpdateInterface.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
|
||||
export interface GroupUpdateInterface {
|
||||
position: PositionInterface,
|
||||
groupId: number,
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isItemEventMessageInterface = new tg.IsInterface()
|
||||
.withProperties({
|
||||
export const isItemEventMessageInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
itemId: tg.isNumber,
|
||||
event: tg.isString,
|
||||
state: tg.isUnknown,
|
||||
parameters: tg.isUnknown,
|
||||
})
|
||||
.get();
|
||||
}).get();
|
||||
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
||||
|
|
11
back/src/Model/Websocket/JoinRoomMessage.ts
Normal file
11
back/src/Model/Websocket/JoinRoomMessage.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
import {isPointInterface} from "./PointInterface";
|
||||
import {isViewport} from "./ViewportMessage";
|
||||
|
||||
export const isJoinRoomMessageInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
roomId: tg.isString,
|
||||
position: isPointInterface,
|
||||
viewport: isViewport
|
||||
}).get();
|
||||
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
6
back/src/Model/Websocket/MessageUserJoined.ts
Normal file
6
back/src/Model/Websocket/MessageUserJoined.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||
|
||||
export class MessageUserJoined {
|
||||
constructor(public userId: number, public name: string, public characterLayers: string[], public position: PointInterface) {
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import { PointInterface } from "./PointInterface";
|
||||
import {PointInterface} from "./PointInterface";
|
||||
|
||||
export class Point implements PointInterface {
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
public direction: string = "none",
|
||||
public moving: boolean = false
|
||||
) {}
|
||||
export class Point implements PointInterface{
|
||||
constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageUserPosition {
|
||||
constructor(public userId: number, public name: string, public characterLayers: string[], public position: PointInterface) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,11 @@ import * as tg from "generic-type-guard";
|
|||
readonly moving: boolean;
|
||||
}*/
|
||||
|
||||
export const isPointInterface = new tg.IsInterface()
|
||||
.withProperties({
|
||||
export const isPointInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
x: tg.isNumber,
|
||||
y: tg.isNumber,
|
||||
direction: tg.isString,
|
||||
moving: tg.isBoolean,
|
||||
})
|
||||
.get();
|
||||
moving: tg.isBoolean
|
||||
}).get();
|
||||
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
import { PointInterface } from "./PointInterface";
|
||||
import {PointInterface} from "./PointInterface";
|
||||
import {
|
||||
CharacterLayerMessage,
|
||||
ItemEventMessage,
|
||||
PointMessage,
|
||||
PositionMessage,
|
||||
PositionMessage
|
||||
} from "../../Messages/generated/messages_pb";
|
||||
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
||||
import {CharacterLayer, ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
|
||||
export class ProtobufUtils {
|
||||
|
||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
||||
let direction: Direction;
|
||||
let direction: PositionMessage.DirectionMap[keyof PositionMessage.DirectionMap];
|
||||
switch (point.direction) {
|
||||
case "up":
|
||||
case 'up':
|
||||
direction = Direction.UP;
|
||||
break;
|
||||
case "down":
|
||||
case 'down':
|
||||
direction = Direction.DOWN;
|
||||
break;
|
||||
case "left":
|
||||
case 'left':
|
||||
direction = Direction.LEFT;
|
||||
break;
|
||||
case "right":
|
||||
case 'right':
|
||||
direction = Direction.RIGHT;
|
||||
break;
|
||||
default:
|
||||
throw new Error("unexpected direction");
|
||||
throw new Error('unexpected direction');
|
||||
}
|
||||
|
||||
const position = new PositionMessage();
|
||||
|
@ -43,16 +44,16 @@ export class ProtobufUtils {
|
|||
let direction: string;
|
||||
switch (position.getDirection()) {
|
||||
case Direction.UP:
|
||||
direction = "up";
|
||||
direction = 'up';
|
||||
break;
|
||||
case Direction.DOWN:
|
||||
direction = "down";
|
||||
direction = 'down';
|
||||
break;
|
||||
case Direction.LEFT:
|
||||
direction = "left";
|
||||
direction = 'left';
|
||||
break;
|
||||
case Direction.RIGHT:
|
||||
direction = "right";
|
||||
direction = 'right';
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unexpected direction");
|
||||
|
@ -81,7 +82,7 @@ export class ProtobufUtils {
|
|||
event: itemEventMessage.getEvent(),
|
||||
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
||||
state: JSON.parse(itemEventMessage.getStatejson()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
||||
|
@ -95,7 +96,7 @@ export class ProtobufUtils {
|
|||
}
|
||||
|
||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
||||
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
||||
return characterLayers.map(function(characterLayer): CharacterLayerMessage {
|
||||
const message = new CharacterLayerMessage();
|
||||
message.setName(characterLayer.name);
|
||||
if (characterLayer.url) {
|
||||
|
@ -104,14 +105,4 @@ export class ProtobufUtils {
|
|||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
|
||||
return characterLayers.map(function (characterLayer): CharacterLayer {
|
||||
const url = characterLayer.getUrl();
|
||||
return {
|
||||
name: characterLayer.getName(),
|
||||
url: url ? url : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
8
back/src/Model/Websocket/SetPlayerDetailsMessage.ts
Normal file
8
back/src/Model/Websocket/SetPlayerDetailsMessage.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isSetPlayerDetailsMessage =
|
||||
new tg.IsInterface().withProperties({
|
||||
name: tg.isString,
|
||||
characterLayers: tg.isArray(tg.isString)
|
||||
}).get();
|
||||
export type SetPlayerDetailsMessage = tg.GuardedType<typeof isSetPlayerDetailsMessage>;
|
5
back/src/Model/Websocket/UserInGroupInterface.ts
Normal file
5
back/src/Model/Websocket/UserInGroupInterface.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export interface UserInGroupInterface {
|
||||
userId: number,
|
||||
name: string,
|
||||
initiator: boolean
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isViewport = new tg.IsInterface()
|
||||
.withProperties({
|
||||
export const isViewport =
|
||||
new tg.IsInterface().withProperties({
|
||||
left: tg.isNumber,
|
||||
top: tg.isNumber,
|
||||
right: tg.isNumber,
|
||||
bottom: tg.isNumber,
|
||||
})
|
||||
.get();
|
||||
}).get();
|
||||
export type ViewportInterface = tg.GuardedType<typeof isViewport>;
|
18
back/src/Model/Websocket/WebRtcSignalMessage.ts
Normal file
18
back/src/Model/Websocket/WebRtcSignalMessage.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isSignalData =
|
||||
new tg.IsInterface().withProperties({
|
||||
type: tg.isOptional(tg.isString)
|
||||
}).get();
|
||||
|
||||
export const isWebRtcSignalMessageInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
receiverId: tg.isNumber,
|
||||
signal: isSignalData
|
||||
}).get();
|
||||
export const isWebRtcScreenSharingStartMessageInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
userId: tg.isNumber,
|
||||
roomId: tg.isString
|
||||
}).get();
|
||||
export type WebRtcSignalMessageInterface = tg.GuardedType<typeof isWebRtcSignalMessageInterface>;
|
|
@ -1,53 +1,36 @@
|
|||
import { User } from "./User";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { Movable } from "./Movable";
|
||||
import { Group } from "./Group";
|
||||
import { ZoneSocket } from "../RoomManager";
|
||||
import {
|
||||
EmoteEventMessage,
|
||||
SetPlayerDetailsMessage,
|
||||
PlayerDetailsUpdatedMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import {User} from "./User";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {Movable} from "./Movable";
|
||||
import {Group} from "./Group";
|
||||
|
||||
export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
|
||||
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
|
||||
export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
|
||||
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
|
||||
export type PlayerDetailsUpdatedCallback = (
|
||||
playerDetailsUpdatedMessage: PlayerDetailsUpdatedMessage,
|
||||
listener: ZoneSocket
|
||||
) => void;
|
||||
export type EntersCallback = (thing: Movable, listener: User) => void;
|
||||
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: User) => void;
|
||||
export type LeavesCallback = (thing: Movable, listener: User) => void;
|
||||
|
||||
export class Zone {
|
||||
private things: Set<Movable> = new Set<Movable>();
|
||||
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
||||
private listeners: Set<User> = new Set<User>();
|
||||
|
||||
constructor(
|
||||
private onEnters: EntersCallback,
|
||||
private onMoves: MovesCallback,
|
||||
private onLeaves: LeavesCallback,
|
||||
private onEmote: EmoteCallback,
|
||||
private onPlayerDetailsUpdated: PlayerDetailsUpdatedCallback,
|
||||
public readonly x: number,
|
||||
public readonly y: number
|
||||
) {}
|
||||
/**
|
||||
* @param x For debugging purpose only
|
||||
* @param y For debugging purpose only
|
||||
*/
|
||||
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private x: number, private y: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A user/thing leaves the zone
|
||||
*/
|
||||
public leave(thing: Movable, newZone: Zone | null) {
|
||||
public leave(thing: Movable, newZone: Zone|null) {
|
||||
const result = this.things.delete(thing);
|
||||
if (!result) {
|
||||
if (thing instanceof User) {
|
||||
throw new Error(`Could not find user in zone ${thing.id}`);
|
||||
throw new Error('Could not find user in zone '+thing.id);
|
||||
}
|
||||
if (thing instanceof Group) {
|
||||
throw new Error(
|
||||
`Could not find group ${thing.getId()} in zone (${this.x},${this.y}). Position of group: (${
|
||||
thing.getPosition().x
|
||||
},${thing.getPosition().y})`
|
||||
);
|
||||
throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')');
|
||||
}
|
||||
|
||||
}
|
||||
this.notifyLeft(thing, newZone);
|
||||
}
|
||||
|
@ -55,13 +38,15 @@ export class Zone {
|
|||
/**
|
||||
* Notify listeners of this zone that this user/thing left
|
||||
*/
|
||||
private notifyLeft(thing: Movable, newZone: Zone | null) {
|
||||
private notifyLeft(thing: Movable, newZone: Zone|null) {
|
||||
for (const listener of this.listeners) {
|
||||
this.onLeaves(thing, newZone, listener);
|
||||
if (listener !== thing && (newZone === null || !listener.listenedZones.has(newZone))) {
|
||||
this.onLeaves(thing, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
||||
public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
|
||||
this.things.add(thing);
|
||||
this.notifyEnter(thing, oldZone, position);
|
||||
}
|
||||
|
@ -69,9 +54,16 @@ export class Zone {
|
|||
/**
|
||||
* Notify listeners of this zone that this user entered
|
||||
*/
|
||||
private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
||||
private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
|
||||
for (const listener of this.listeners) {
|
||||
this.onEnters(thing, oldZone, listener);
|
||||
if (listener === thing) {
|
||||
continue;
|
||||
}
|
||||
if (oldZone === null || !listener.listenedZones.has(oldZone)) {
|
||||
this.onEnters(thing, listener);
|
||||
} else {
|
||||
this.onMoves(thing, position, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,38 +75,35 @@ export class Zone {
|
|||
}
|
||||
|
||||
for (const listener of this.listeners) {
|
||||
//if (listener !== thing) {
|
||||
this.onMoves(thing, position, listener);
|
||||
//}
|
||||
if (listener !== thing) {
|
||||
this.onMoves(thing,position, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public startListening(listener: User): void {
|
||||
for (const thing of this.things) {
|
||||
if (thing !== listener) {
|
||||
this.onEnters(thing, listener);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners.add(listener);
|
||||
listener.listenedZones.add(this);
|
||||
}
|
||||
|
||||
public stopListening(listener: User): void {
|
||||
for (const thing of this.things) {
|
||||
if (thing !== listener) {
|
||||
this.onLeaves(thing, listener);
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners.delete(listener);
|
||||
listener.listenedZones.delete(this);
|
||||
}
|
||||
|
||||
public getThings(): Set<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 +1,13 @@
|
|||
import { App as _App, AppOptions } from "uWebSockets.js";
|
||||
import BaseApp from "./baseapp";
|
||||
import { extend } from "./utils";
|
||||
import { UwsApp } from "./types";
|
||||
import { App as _App, AppOptions } from 'uWebSockets.js';
|
||||
import BaseApp from './baseapp';
|
||||
import { extend } from './utils';
|
||||
import { UwsApp } from './types';
|
||||
|
||||
class App extends (<UwsApp>_App) {
|
||||
constructor(options: AppOptions = {}) {
|
||||
super(options); // eslint-disable-line constructor-super
|
||||
extend(this, new BaseApp());
|
||||
}
|
||||
constructor(options: AppOptions = {}) {
|
||||
super(options); // eslint-disable-line constructor-super
|
||||
extend(this, new BaseApp());
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -1,111 +1,116 @@
|
|||
/* eslint-disable */
|
||||
import { Readable } from 'stream';
|
||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
||||
|
||||
import { Readable } from "stream";
|
||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||
import formData from './formdata';
|
||||
import { stob } from './utils';
|
||||
import { Handler } from './types';
|
||||
import {join} from "path";
|
||||
|
||||
import formData from "./formdata";
|
||||
import { stob } from "./utils";
|
||||
import { Handler } from "./types";
|
||||
import { join } from "path";
|
||||
|
||||
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
|
||||
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
|
||||
const noOp = () => true;
|
||||
|
||||
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
||||
const contType = req.getHeader("content-type");
|
||||
const contType = req.getHeader('content-type');
|
||||
|
||||
res.bodyStream = function () {
|
||||
const stream = new Readable();
|
||||
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
||||
res.bodyStream = function() {
|
||||
const stream = new Readable();
|
||||
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
||||
|
||||
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
||||
// uint and then slicing is bit faster than slice and then uint
|
||||
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
if (isLast) {
|
||||
stream.push(null);
|
||||
}
|
||||
});
|
||||
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
||||
// uint and then slicing is bit faster than slice and then uint
|
||||
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
if (isLast) {
|
||||
stream.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
return stream;
|
||||
};
|
||||
return stream;
|
||||
};
|
||||
|
||||
res.body = () => stob(res.bodyStream());
|
||||
res.body = () => stob(res.bodyStream());
|
||||
|
||||
if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
|
||||
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
|
||||
if (contType.includes('application/json'))
|
||||
res.json = async () => JSON.parse(await res.body());
|
||||
if (contTypes.map(t => contType.includes(t)).includes(true))
|
||||
res.formData = formData.bind(res, contType);
|
||||
};
|
||||
|
||||
class BaseApp {
|
||||
_sockets = new Map();
|
||||
ws!: TemplatedApp["ws"];
|
||||
get!: TemplatedApp["get"];
|
||||
_post!: TemplatedApp["post"];
|
||||
_put!: TemplatedApp["put"];
|
||||
_patch!: TemplatedApp["patch"];
|
||||
_listen!: TemplatedApp["listen"];
|
||||
_sockets = new Map();
|
||||
ws!: TemplatedApp['ws'];
|
||||
get!: TemplatedApp['get'];
|
||||
_post!: TemplatedApp['post'];
|
||||
_put!: TemplatedApp['put'];
|
||||
_patch!: TemplatedApp['patch'];
|
||||
_listen!: TemplatedApp['listen'];
|
||||
|
||||
post(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._post(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
post(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== 'function')
|
||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._post(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
put(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._put(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
put(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== 'function')
|
||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._put(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
patch(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._patch(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
patch(pattern: string, handler: Handler) {
|
||||
if (typeof handler !== 'function')
|
||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||
this._patch(pattern, (res, req) => {
|
||||
handleBody(res, req);
|
||||
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
handler(res, req);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
||||
if (typeof p === "number" && typeof h === "string") {
|
||||
this._listen(h, p, (socket) => {
|
||||
this._sockets.set(p, socket);
|
||||
if (cb === undefined) {
|
||||
throw new Error("cb undefined");
|
||||
}
|
||||
cb(socket);
|
||||
});
|
||||
} else if (typeof h === "number" && typeof p === "function") {
|
||||
this._listen(h, (socket) => {
|
||||
this._sockets.set(h, socket);
|
||||
p(socket);
|
||||
});
|
||||
} else {
|
||||
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
|
||||
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
||||
if (typeof p === 'number' && typeof h === 'string') {
|
||||
this._listen(h, p, socket => {
|
||||
this._sockets.set(p, socket);
|
||||
if (cb === undefined) {
|
||||
throw new Error('cb undefined');
|
||||
}
|
||||
|
||||
return this;
|
||||
cb(socket);
|
||||
});
|
||||
} else if (typeof h === 'number' && typeof p === 'function') {
|
||||
this._listen(h, socket => {
|
||||
this._sockets.set(h, socket);
|
||||
p(socket);
|
||||
});
|
||||
} else {
|
||||
throw Error(
|
||||
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
|
||||
);
|
||||
}
|
||||
|
||||
close(port: null | number = null) {
|
||||
if (port) {
|
||||
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
||||
this._sockets.delete(port);
|
||||
} else {
|
||||
this._sockets.forEach((app) => {
|
||||
us_listen_socket_close(app);
|
||||
});
|
||||
this._sockets.clear();
|
||||
}
|
||||
return this;
|
||||
return this;
|
||||
}
|
||||
|
||||
close(port: null | number = null) {
|
||||
if (port) {
|
||||
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
||||
this._sockets.delete(port);
|
||||
} else {
|
||||
this._sockets.forEach(app => {
|
||||
us_listen_socket_close(app);
|
||||
});
|
||||
this._sockets.clear();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseApp;
|
||||
|
|
|
@ -1,101 +1,100 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import { createWriteStream } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import Busboy from "busboy";
|
||||
import mkdirp from "mkdirp";
|
||||
import { createWriteStream } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import Busboy from 'busboy';
|
||||
import mkdirp from 'mkdirp';
|
||||
|
||||
function formData(
|
||||
contType: string,
|
||||
options: busboy.BusboyConfig & {
|
||||
abortOnLimit?: boolean;
|
||||
tmpDir?: string;
|
||||
onFile?: (
|
||||
fieldname: string,
|
||||
file: NodeJS.ReadableStream,
|
||||
filename: string,
|
||||
encoding: string,
|
||||
mimetype: string
|
||||
) => string;
|
||||
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
filename?: (oldName: string) => string;
|
||||
} = {}
|
||||
contType: string,
|
||||
options: busboy.BusboyConfig & {
|
||||
abortOnLimit?: boolean;
|
||||
tmpDir?: string;
|
||||
onFile?: (
|
||||
fieldname: string,
|
||||
file: NodeJS.ReadableStream,
|
||||
filename: string,
|
||||
encoding: string,
|
||||
mimetype: string
|
||||
) => string;
|
||||
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
filename?: (oldName: string) => string;
|
||||
} = {}
|
||||
) {
|
||||
console.log("Enter form data");
|
||||
options.headers = {
|
||||
"content-type": contType,
|
||||
};
|
||||
console.log('Enter form data');
|
||||
options.headers = {
|
||||
'content-type': contType
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const busb = new Busboy(options);
|
||||
const ret = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
const busb = new Busboy(options);
|
||||
const ret = {};
|
||||
|
||||
this.bodyStream().pipe(busb);
|
||||
this.bodyStream().pipe(busb);
|
||||
|
||||
busb.on("limit", () => {
|
||||
if (options.abortOnLimit) {
|
||||
reject(Error("limit"));
|
||||
}
|
||||
});
|
||||
|
||||
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
|
||||
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
|
||||
filename,
|
||||
encoding,
|
||||
mimetype,
|
||||
filePath: undefined,
|
||||
};
|
||||
|
||||
if (typeof options.tmpDir === "string") {
|
||||
if (typeof options.filename === "function") filename = options.filename(filename);
|
||||
const fileToSave = join(options.tmpDir, filename);
|
||||
mkdirp(dirname(fileToSave));
|
||||
|
||||
file.pipe(createWriteStream(fileToSave));
|
||||
value.filePath = fileToSave;
|
||||
}
|
||||
if (typeof options.onFile === "function") {
|
||||
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
||||
}
|
||||
|
||||
setRetValue(ret, fieldname, value);
|
||||
});
|
||||
|
||||
busb.on("field", function (fieldname, value) {
|
||||
if (typeof options.onField === "function") options.onField(fieldname, value);
|
||||
|
||||
setRetValue(ret, fieldname, value);
|
||||
});
|
||||
|
||||
busb.on("finish", function () {
|
||||
resolve(ret);
|
||||
});
|
||||
|
||||
busb.on("error", reject);
|
||||
busb.on('limit', () => {
|
||||
if (options.abortOnLimit) {
|
||||
reject(Error('limit'));
|
||||
}
|
||||
});
|
||||
|
||||
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
||||
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = {
|
||||
filename,
|
||||
encoding,
|
||||
mimetype,
|
||||
filePath: undefined
|
||||
};
|
||||
|
||||
if (typeof options.tmpDir === 'string') {
|
||||
if (typeof options.filename === 'function') filename = options.filename(filename);
|
||||
const fileToSave = join(options.tmpDir, filename);
|
||||
mkdirp(dirname(fileToSave));
|
||||
|
||||
file.pipe(createWriteStream(fileToSave));
|
||||
value.filePath = fileToSave;
|
||||
}
|
||||
if (typeof options.onFile === 'function') {
|
||||
value.filePath =
|
||||
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
||||
}
|
||||
|
||||
setRetValue(ret, fieldname, value);
|
||||
});
|
||||
|
||||
busb.on('field', function(fieldname, value) {
|
||||
if (typeof options.onField === 'function') options.onField(fieldname, value);
|
||||
|
||||
setRetValue(ret, fieldname, value);
|
||||
});
|
||||
|
||||
busb.on('finish', function() {
|
||||
resolve(ret);
|
||||
});
|
||||
|
||||
busb.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function setRetValue(
|
||||
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
fieldname: string,
|
||||
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
fieldname: string,
|
||||
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
) {
|
||||
if (fieldname.endsWith("[]")) {
|
||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
||||
if (Array.isArray(ret[fieldname])) {
|
||||
ret[fieldname].push(value);
|
||||
} else {
|
||||
ret[fieldname] = [value];
|
||||
}
|
||||
if (fieldname.endsWith('[]')) {
|
||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
||||
if (Array.isArray(ret[fieldname])) {
|
||||
ret[fieldname].push(value);
|
||||
} else {
|
||||
if (Array.isArray(ret[fieldname])) {
|
||||
ret[fieldname].push(value);
|
||||
} else if (ret[fieldname]) {
|
||||
ret[fieldname] = [ret[fieldname], value];
|
||||
} else {
|
||||
ret[fieldname] = value;
|
||||
}
|
||||
ret[fieldname] = [value];
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(ret[fieldname])) {
|
||||
ret[fieldname].push(value);
|
||||
} else if (ret[fieldname]) {
|
||||
ret[fieldname] = [ret[fieldname], value];
|
||||
} else {
|
||||
ret[fieldname] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default formData;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
|
||||
import BaseApp from "./baseapp";
|
||||
import { extend } from "./utils";
|
||||
import { UwsApp } from "./types";
|
||||
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js';
|
||||
import BaseApp from './baseapp';
|
||||
import { extend } from './utils';
|
||||
import { UwsApp } from './types';
|
||||
|
||||
class SSLApp extends (<UwsApp>_SSLApp) {
|
||||
constructor(options: AppOptions) {
|
||||
super(options); // eslint-disable-line constructor-super
|
||||
extend(this, new BaseApp());
|
||||
}
|
||||
constructor(options: AppOptions) {
|
||||
super(options); // eslint-disable-line constructor-super
|
||||
extend(this, new BaseApp());
|
||||
}
|
||||
}
|
||||
|
||||
export default SSLApp;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
||||
|
||||
export type UwsApp = {
|
||||
(options: AppOptions): TemplatedApp;
|
||||
new (options: AppOptions): TemplatedApp;
|
||||
prototype: TemplatedApp;
|
||||
(options: AppOptions): TemplatedApp;
|
||||
new (options: AppOptions): TemplatedApp;
|
||||
prototype: TemplatedApp;
|
||||
};
|
||||
|
||||
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
/* eslint-disable */
|
||||
import { ReadStream } from 'fs';
|
||||
|
||||
import { ReadStream } from "fs";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function extend(who: any, from: any, overwrite = true) {
|
||||
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
|
||||
ownProps.forEach((prop) => {
|
||||
if (prop === "constructor" || from[prop] === undefined) return;
|
||||
if (who[prop] && overwrite) {
|
||||
who[`_${prop}`] = who[prop];
|
||||
}
|
||||
if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
|
||||
else who[prop] = from[prop];
|
||||
});
|
||||
function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(
|
||||
Object.keys(from)
|
||||
);
|
||||
ownProps.forEach(prop => {
|
||||
if (prop === 'constructor' || from[prop] === undefined) return;
|
||||
if (who[prop] && overwrite) {
|
||||
who[`_${prop}`] = who[prop];
|
||||
}
|
||||
if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who);
|
||||
else who[prop] = from[prop];
|
||||
});
|
||||
}
|
||||
|
||||
function stob(stream: ReadStream): Promise<Buffer> {
|
||||
return new Promise((resolve) => {
|
||||
const buffers: Buffer[] = [];
|
||||
stream.on("data", buffers.push.bind(buffers));
|
||||
return new Promise(resolve => {
|
||||
const buffers: Buffer[] = [];
|
||||
stream.on('data', buffers.push.bind(buffers));
|
||||
|
||||
stream.on("end", () => {
|
||||
switch (buffers.length) {
|
||||
case 0:
|
||||
resolve(Buffer.allocUnsafe(0));
|
||||
break;
|
||||
case 1:
|
||||
resolve(buffers[0]);
|
||||
break;
|
||||
default:
|
||||
resolve(Buffer.concat(buffers));
|
||||
}
|
||||
});
|
||||
stream.on('end', () => {
|
||||
switch (buffers.length) {
|
||||
case 0:
|
||||
resolve(Buffer.allocUnsafe(0));
|
||||
break;
|
||||
case 1:
|
||||
resolve(buffers[0]);
|
||||
break;
|
||||
default:
|
||||
resolve(Buffer.concat(buffers));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export { extend, stob };
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { parse } from "query-string";
|
||||
import { HttpRequest } from "uWebSockets.js";
|
||||
import App from "./server/app";
|
||||
import SSLApp from "./server/sslapp";
|
||||
import * as types from "./server/types";
|
||||
import { parse } from 'query-string';
|
||||
import { HttpRequest } from 'uWebSockets.js';
|
||||
import App from './server/app';
|
||||
import SSLApp from './server/sslapp';
|
||||
import * as types from './server/types';
|
||||
|
||||
const getQuery = (req: HttpRequest) => {
|
||||
return parse(req.getQuery());
|
||||
return parse(req.getQuery());
|
||||
};
|
||||
|
||||
export { App, SSLApp, getQuery };
|
||||
export * from "./server/types";
|
||||
export * from './server/types';
|
||||
|
||||
export default {
|
||||
App,
|
||||
SSLApp,
|
||||
getQuery,
|
||||
...types,
|
||||
App,
|
||||
SSLApp,
|
||||
getQuery,
|
||||
...types
|
||||
};
|
||||
|
|
|
@ -1,30 +1,115 @@
|
|||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
|
||||
import Axios from "axios";
|
||||
import { isMapDetailsData, MapDetailsData } from "./AdminApi/MapDetailsData";
|
||||
import { isRoomRedirect, RoomRedirect } from "./AdminApi/RoomRedirect";
|
||||
import {v4} from "uuid";
|
||||
|
||||
export interface AdminApiData {
|
||||
organizationSlug: string
|
||||
worldSlug: string
|
||||
roomSlug: string
|
||||
mapUrlStart: string
|
||||
tags: string[]
|
||||
policy_type: number
|
||||
userUuid: string
|
||||
messages?: unknown[],
|
||||
textures: CharacterTexture[]
|
||||
}
|
||||
|
||||
export interface CharacterTexture {
|
||||
id: number,
|
||||
level: number,
|
||||
url: string,
|
||||
rights: string
|
||||
}
|
||||
|
||||
export interface FetchMemberDataByUuidResponse {
|
||||
uuid: string;
|
||||
tags: string[];
|
||||
textures: CharacterTexture[];
|
||||
messages: unknown[];
|
||||
}
|
||||
|
||||
class AdminApi {
|
||||
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
||||
|
||||
async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise<AdminApiData> {
|
||||
if (!ADMIN_API_URL) {
|
||||
return Promise.reject(new Error("No admin backoffice set!"));
|
||||
return Promise.reject('No admin backoffice set!');
|
||||
}
|
||||
|
||||
const params: { playUri: string } = {
|
||||
playUri,
|
||||
const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
|
||||
organizationSlug,
|
||||
worldSlug
|
||||
};
|
||||
|
||||
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||
params,
|
||||
});
|
||||
|
||||
if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) {
|
||||
console.error("Unexpected answer from the /api/map admin endpoint.", res.data);
|
||||
throw new Error("Unexpected answer from the /api/map admin endpoint.");
|
||||
if (roomSlug) {
|
||||
params.roomSlug = roomSlug;
|
||||
}
|
||||
|
||||
const res = await Axios.get(ADMIN_API_URL + '/api/map',
|
||||
{
|
||||
headers: {"Authorization": `${ADMIN_API_TOKEN}`},
|
||||
params
|
||||
}
|
||||
)
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async fetchMemberDataByUuid(uuid: string): Promise<FetchMemberDataByUuidResponse> {
|
||||
if (!ADMIN_API_URL) {
|
||||
return Promise.reject('No admin backoffice set!');
|
||||
}
|
||||
try {
|
||||
const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid,
|
||||
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||
)
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
if (e?.response?.status == 404) {
|
||||
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
|
||||
console.warn('Cannot find user with uuid "'+uuid+'". Performing an anonymous login instead.');
|
||||
return {
|
||||
uuid: v4(),
|
||||
tags: [],
|
||||
textures: [],
|
||||
messages: [],
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> {
|
||||
if (!ADMIN_API_URL) {
|
||||
return Promise.reject('No admin backoffice set!');
|
||||
}
|
||||
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
||||
const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken,
|
||||
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||
)
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> {
|
||||
if (!ADMIN_API_URL) {
|
||||
return Promise.reject('No admin backoffice set!');
|
||||
}
|
||||
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
||||
const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken,
|
||||
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} }
|
||||
)
|
||||
return res.data;
|
||||
}
|
||||
|
||||
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) {
|
||||
return Axios.post(`${ADMIN_API_URL}/api/report`, {
|
||||
reportedUserUuid,
|
||||
reportedUserComment,
|
||||
reporterUserUuid,
|
||||
},
|
||||
{
|
||||
headers: {"Authorization": `${ADMIN_API_TOKEN}`}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const adminApi = new AdminApi();
|
||||
|
|
|
@ -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 +1,3 @@
|
|||
export const arrayIntersect = (array1: string[], array2: string[]): boolean => {
|
||||
return array1.filter((value) => array2.includes(value)).length > 0;
|
||||
};
|
||||
export const arrayIntersect = (array1: string[], array2: string[]) : boolean => {
|
||||
return array1.filter(value => array2.includes(value)).length > 0;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { EventEmitter } from "events";
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const clientJoinEvent = "clientJoin";
|
||||
const clientLeaveEvent = "clientLeave";
|
||||
const clientJoinEvent = 'clientJoin';
|
||||
const clientLeaveEvent = 'clientLeave';
|
||||
|
||||
class ClientEventsEmitter extends EventEmitter {
|
||||
emitClientJoin(clientUUid: string, roomId: string): void {
|
||||
|
@ -11,7 +11,7 @@ class ClientEventsEmitter extends EventEmitter {
|
|||
emitClientLeave(clientUUid: string, roomId: string): void {
|
||||
this.emit(clientLeaveEvent, clientUUid, roomId);
|
||||
}
|
||||
|
||||
|
||||
registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void {
|
||||
this.on(clientJoinEvent, callback);
|
||||
}
|
||||
|
@ -29,4 +29,4 @@ class ClientEventsEmitter extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
export const clientEventsEmitter = new ClientEventsEmitter();
|
||||
export const clientEventsEmitter = new ClientEventsEmitter();
|
|
@ -1,6 +1,6 @@
|
|||
import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
|
||||
import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable";
|
||||
|
||||
function secNSec2ms(secNSec: Array<number> | number) {
|
||||
function secNSec2ms(secNSec: Array<number>|number) {
|
||||
if (Array.isArray(secNSec)) {
|
||||
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
||||
}
|
||||
|
@ -12,17 +12,17 @@ class CpuTracker {
|
|||
private overHeating: boolean = false;
|
||||
|
||||
constructor() {
|
||||
let time = process.hrtime.bigint();
|
||||
let usage = process.cpuUsage();
|
||||
let time = process.hrtime.bigint()
|
||||
let usage = process.cpuUsage()
|
||||
setInterval(() => {
|
||||
const elapTime = process.hrtime.bigint();
|
||||
const elapUsage = process.cpuUsage(usage);
|
||||
usage = process.cpuUsage();
|
||||
const elapUsage = process.cpuUsage(usage)
|
||||
usage = process.cpuUsage()
|
||||
|
||||
const elapTimeMS = elapTime - time;
|
||||
const elapUserMS = secNSec2ms(elapUsage.user);
|
||||
const elapSystMS = secNSec2ms(elapUsage.system);
|
||||
this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
|
||||
const elapUserMS = secNSec2ms(elapUsage.user)
|
||||
const elapSystMS = secNSec2ms(elapUsage.system)
|
||||
this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000)
|
||||
|
||||
time = elapTime;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Counter, Gauge } from "prom-client";
|
||||
import {Counter, Gauge} from "prom-client";
|
||||
|
||||
//this class should manage all the custom metrics used by prometheus
|
||||
class GaugeManager {
|
||||
|
@ -10,29 +10,29 @@ class GaugeManager {
|
|||
|
||||
constructor() {
|
||||
this.nbRoomsGauge = new Gauge({
|
||||
name: "workadventure_nb_rooms",
|
||||
help: "Number of active rooms",
|
||||
name: 'workadventure_nb_rooms',
|
||||
help: 'Number of active rooms'
|
||||
});
|
||||
this.nbClientsGauge = new Gauge({
|
||||
name: "workadventure_nb_sockets",
|
||||
help: "Number of connected sockets",
|
||||
labelNames: [],
|
||||
name: 'workadventure_nb_sockets',
|
||||
help: 'Number of connected sockets',
|
||||
labelNames: [ ]
|
||||
});
|
||||
this.nbClientsPerRoomGauge = new Gauge({
|
||||
name: "workadventure_nb_clients_per_room",
|
||||
help: "Number of clients per room",
|
||||
labelNames: ["room"],
|
||||
name: 'workadventure_nb_clients_per_room',
|
||||
help: 'Number of clients per room',
|
||||
labelNames: [ 'room' ]
|
||||
});
|
||||
|
||||
this.nbGroupsPerRoomCounter = new Counter({
|
||||
name: "workadventure_counter_groups_per_room",
|
||||
help: "Counter of groups per room",
|
||||
labelNames: ["room"],
|
||||
name: 'workadventure_counter_groups_per_room',
|
||||
help: 'Counter of groups per room',
|
||||
labelNames: [ 'room' ]
|
||||
});
|
||||
this.nbGroupsPerRoomGauge = new Gauge({
|
||||
name: "workadventure_nb_groups_per_room",
|
||||
help: "Number of groups per room",
|
||||
labelNames: ["room"],
|
||||
name: 'workadventure_nb_groups_per_room',
|
||||
help: 'Number of groups per room',
|
||||
labelNames: [ 'room' ]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,15 @@ class GaugeManager {
|
|||
this.nbClientsGauge.dec();
|
||||
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
||||
}
|
||||
|
||||
incNbGroupsPerRoomGauge(roomId: string): void {
|
||||
this.nbGroupsPerRoomCounter.inc({ room: roomId })
|
||||
this.nbGroupsPerRoomGauge.inc({ room: roomId })
|
||||
}
|
||||
|
||||
decNbGroupsPerRoomGauge(roomId: string): void {
|
||||
this.nbGroupsPerRoomGauge.dec({ room: roomId })
|
||||
}
|
||||
}
|
||||
|
||||
export const gaugeManager = new GaugeManager();
|
||||
export const gaugeManager = new GaugeManager();
|
|
@ -1,6 +1,5 @@
|
|||
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||
import { BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage } from "../Messages/generated/messages_pb";
|
||||
import { WebSocket } from "uWebSockets.js";
|
||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||
import {BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb";
|
||||
|
||||
export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void {
|
||||
socket.batchedMessages.addPayload(payload);
|
||||
|
@ -21,7 +20,7 @@ export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): voi
|
|||
}
|
||||
}
|
||||
|
||||
export function emitError(Client: WebSocket, message: string): void {
|
||||
export function emitError(Client: ExSocketInterface, message: string): void {
|
||||
const errorMessage = new ErrorMessage();
|
||||
errorMessage.setMessage(message);
|
||||
|
||||
|
@ -33,3 +32,4 @@ export function emitError(Client: WebSocket, message: string): void {
|
|||
}
|
||||
console.warn(message);
|
||||
}
|
||||
|
76
back/src/Services/JWTTokenManager.ts
Normal file
76
back/src/Services/JWTTokenManager.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable";
|
||||
import {uuid} from "uuidv4";
|
||||
import Jwt from "jsonwebtoken";
|
||||
import {TokenInterface} from "../Controller/AuthenticateController";
|
||||
import {adminApi, AdminApiData} from "../Services/AdminApi";
|
||||
|
||||
class JWTTokenManager {
|
||||
|
||||
public createJWTToken(userUuid: string) {
|
||||
return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token
|
||||
}
|
||||
|
||||
public async getUserUuidFromToken(token: unknown): Promise<string> {
|
||||
|
||||
if (!token) {
|
||||
throw new Error('An authentication error happened, a user tried to connect without a token.');
|
||||
}
|
||||
if (typeof(token) !== "string") {
|
||||
throw new Error('Token is expected to be a string');
|
||||
}
|
||||
|
||||
|
||||
if(token === 'test') {
|
||||
if (ALLOW_ARTILLERY) {
|
||||
return uuid();
|
||||
} else {
|
||||
throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'");
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => {
|
||||
const tokenInterface = tokenDecoded as TokenInterface;
|
||||
if (err) {
|
||||
console.error('An authentication error happened, invalid JsonWebToken.', err);
|
||||
reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message));
|
||||
return;
|
||||
}
|
||||
if (tokenDecoded === undefined) {
|
||||
console.error('Empty token found.');
|
||||
reject(new Error('Empty token found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
//verify token
|
||||
if (!this.isValidToken(tokenInterface)) {
|
||||
reject(new Error('Authentication error, invalid token structure.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ADMIN_API_URL) {
|
||||
//verify user in admin
|
||||
adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => {
|
||||
resolve(tokenInterface.userUuid);
|
||||
}).catch((err) => {
|
||||
//anonymous user
|
||||
if(err.response && err.response.status && err.response.status === 404){
|
||||
resolve(tokenInterface.userUuid);
|
||||
return;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
resolve(tokenInterface.userUuid);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private isValidToken(token: object): token is TokenInterface {
|
||||
return !(typeof((token as TokenInterface).userUuid) !== 'string');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const jwtTokenManager = new JWTTokenManager();
|
|
@ -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);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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,148 +1,97 @@
|
|||
import "jasmine";
|
||||
import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
|
||||
import { Point } from "../src/Model/Websocket/MessageUserPosition";
|
||||
import {GameRoom, ConnectCallback, DisconnectCallback } from "../src/Model/GameRoom";
|
||||
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||
import { Group } from "../src/Model/Group";
|
||||
import { User, UserSocket } from "_Model/User";
|
||||
import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import { EmoteCallback } from "_Model/Zone";
|
||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||
import {User} from "_Model/User";
|
||||
|
||||
function createMockUser(userId: number): User {
|
||||
function createMockUser(userId: number): ExSocketInterface {
|
||||
return {
|
||||
userId,
|
||||
} as unknown as User;
|
||||
userId
|
||||
} as ExSocketInterface;
|
||||
}
|
||||
|
||||
function createMockUserSocket(): UserSocket {
|
||||
return {} as unknown as UserSocket;
|
||||
}
|
||||
|
||||
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage {
|
||||
const positionMessage = new PositionMessage();
|
||||
positionMessage.setX(x);
|
||||
positionMessage.setY(y);
|
||||
positionMessage.setDirection(Direction.DOWN);
|
||||
positionMessage.setMoving(false);
|
||||
const joinRoomMessage = new JoinRoomMessage();
|
||||
joinRoomMessage.setUseruuid("1");
|
||||
joinRoomMessage.setIpaddress("10.0.0.2");
|
||||
joinRoomMessage.setName("foo");
|
||||
joinRoomMessage.setRoomid("_/global/test.json");
|
||||
joinRoomMessage.setPositionmessage(positionMessage);
|
||||
return joinRoomMessage;
|
||||
}
|
||||
|
||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {};
|
||||
|
||||
describe("GameRoom", () => {
|
||||
it("should connect user1 and user2", async () => {
|
||||
it("should connect user1 and user2", () => {
|
||||
let connectCalledNumber: number = 0;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
connectCalledNumber++;
|
||||
};
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
||||
}
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
|
||||
const world = await GameRoom.create(
|
||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||
connect,
|
||||
disconnect,
|
||||
160,
|
||||
160,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100));
|
||||
world.join(createMockUser(1), new Point(100, 100));
|
||||
|
||||
world.updatePosition(user2, new Point(261, 100));
|
||||
world.join(createMockUser(2), new Point(500, 100));
|
||||
|
||||
world.updatePosition({ userId: 2 }, new Point(261, 100));
|
||||
|
||||
expect(connectCalledNumber).toBe(0);
|
||||
|
||||
world.updatePosition(user2, new Point(101, 100));
|
||||
world.updatePosition({ userId: 2 }, new Point(101, 100));
|
||||
|
||||
expect(connectCalledNumber).toBe(2);
|
||||
|
||||
world.updatePosition(user2, new Point(102, 100));
|
||||
world.updatePosition({ userId: 2 }, new Point(102, 100));
|
||||
expect(connectCalledNumber).toBe(2);
|
||||
});
|
||||
|
||||
it("should connect 3 users", async () => {
|
||||
it("should connect 3 users", () => {
|
||||
let connectCalled: boolean = false;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
connectCalled = true;
|
||||
};
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
||||
}
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
|
||||
const world = await GameRoom.create(
|
||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||
connect,
|
||||
disconnect,
|
||||
160,
|
||||
160,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 200, 100));
|
||||
world.join(createMockUser(1), new Point(100, 100));
|
||||
|
||||
world.join(createMockUser(2), new Point(200, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
connectCalled = false;
|
||||
|
||||
// baz joins at the outer limit of the group
|
||||
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 311, 100));
|
||||
world.join(createMockUser(3), new Point(311, 100));
|
||||
|
||||
expect(connectCalled).toBe(false);
|
||||
|
||||
world.updatePosition(user3, new Point(309, 100));
|
||||
world.updatePosition({ userId: 3 }, new Point(309, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disconnect user1 and user2", async () => {
|
||||
it("should disconnect user1 and user2", () => {
|
||||
let connectCalled: boolean = false;
|
||||
let disconnectCallNumber: number = 0;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
connectCalled = true;
|
||||
};
|
||||
}
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
disconnectCallNumber++;
|
||||
};
|
||||
}
|
||||
|
||||
const world = await GameRoom.create(
|
||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||
connect,
|
||||
disconnect,
|
||||
160,
|
||||
160,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote,
|
||||
() => {}
|
||||
);
|
||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
world.join(createMockUser(1), new Point(100, 100));
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100));
|
||||
world.join(createMockUser(2), new Point(259, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
expect(disconnectCallNumber).toBe(0);
|
||||
|
||||
world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100));
|
||||
world.updatePosition({ userId: 2 }, new Point(100+160+160+1, 100));
|
||||
|
||||
expect(disconnectCallNumber).toBe(2);
|
||||
|
||||
world.updatePosition(user2, new Point(262, 100));
|
||||
world.updatePosition({ userId: 2 }, new Point(262, 100));
|
||||
expect(disconnectCallNumber).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -1,10 +1,15 @@
|
|||
import "jasmine";
|
||||
import { PositionNotifier } from "../src/Model/PositionNotifier";
|
||||
import { User, UserSocket } from "../src/Model/User";
|
||||
import { Zone } from "_Model/Zone";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { PositionInterface } from "_Model/PositionInterface";
|
||||
import { ZoneSocket } from "../src/RoomManager";
|
||||
import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom";
|
||||
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||
import { Group } from "../src/Model/Group";
|
||||
import {PositionNotifier} from "../src/Model/PositionNotifier";
|
||||
import {User} from "../src/Model/User";
|
||||
import {PointInterface} from "../src/Model/Websocket/PointInterface";
|
||||
import {Zone} from "_Model/Zone";
|
||||
import {Movable} from "_Model/Movable";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||
|
||||
|
||||
describe("PositionNotifier", () => {
|
||||
it("should receive notifications when player moves", () => {
|
||||
|
@ -12,86 +17,50 @@ describe("PositionNotifier", () => {
|
|||
let moveTriggered = false;
|
||||
let leaveTriggered = false;
|
||||
|
||||
const positionNotifier = new PositionNotifier(
|
||||
300,
|
||||
300,
|
||||
(thing: Movable) => {
|
||||
enterTriggered = true;
|
||||
},
|
||||
(thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
},
|
||||
(thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
},
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => {
|
||||
enterTriggered = true;
|
||||
}, (thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
}, (thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
});
|
||||
|
||||
const user1 = new User(
|
||||
1,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
const user1 = new User(1, 'test', {
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: 'down'
|
||||
}, false, positionNotifier, {} as ExSocketInterface);
|
||||
|
||||
const user2 = new User(
|
||||
2,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: -9999,
|
||||
y: -9999,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
const user2 = new User(2, 'test', {
|
||||
x: -9999,
|
||||
y: -9999,
|
||||
moving: false,
|
||||
direction: 'down'
|
||||
}, false, positionNotifier, {} as ExSocketInterface);
|
||||
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 1, 1);
|
||||
positionNotifier.addZoneListener({} as ZoneSocket, 1, 0);
|
||||
/*positionNotifier.setViewport(user1, {
|
||||
positionNotifier.setViewport(user1, {
|
||||
left: 200,
|
||||
right: 600,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
});
|
||||
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
enterTriggered = false;
|
||||
|
||||
// Move inside the zone
|
||||
user2.setPosition({ x: 501, y: 500, direction: "down", moving: false });
|
||||
user2.setPosition({x:501, y:500, direction: 'down', moving: false});
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(true);
|
||||
moveTriggered = false;
|
||||
|
||||
// Move out of the zone in a zone that we don't track
|
||||
user2.setPosition({ x: 901, y: 500, direction: "down", moving: false });
|
||||
user2.setPosition({x: 901, y: 500, direction: 'down', moving: false});
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
|
@ -99,15 +68,22 @@ describe("PositionNotifier", () => {
|
|||
leaveTriggered = false;
|
||||
|
||||
// Move back in
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
enterTriggered = false;
|
||||
|
||||
// Move out of the zone in a zone that we do track
|
||||
user2.setPosition({x: 200, y: 500, direction: 'down', moving: false});
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(true);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
moveTriggered = false;
|
||||
|
||||
// Leave the room
|
||||
positionNotifier.leave(user2);
|
||||
//positionNotifier.removeViewport(user2);
|
||||
positionNotifier.removeViewport(user2);
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(true);
|
||||
|
@ -119,128 +95,82 @@ describe("PositionNotifier", () => {
|
|||
let moveTriggered = false;
|
||||
let leaveTriggered = false;
|
||||
|
||||
const positionNotifier = new PositionNotifier(
|
||||
300,
|
||||
300,
|
||||
(thing: Movable, fromZone: Zone | null) => {
|
||||
enterTriggered = true;
|
||||
},
|
||||
(thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
},
|
||||
(thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
},
|
||||
() => {},
|
||||
() => {}
|
||||
);
|
||||
const positionNotifier = new PositionNotifier(300, 300, (thing: Movable) => {
|
||||
enterTriggered = true;
|
||||
}, (thing: Movable, position: PositionInterface) => {
|
||||
moveTriggered = true;
|
||||
}, (thing: Movable) => {
|
||||
leaveTriggered = true;
|
||||
});
|
||||
|
||||
const user1 = new User(
|
||||
1,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
const user1 = new User(1, 'test', {
|
||||
x: 500,
|
||||
y: 500,
|
||||
moving: false,
|
||||
direction: 'down'
|
||||
}, false, positionNotifier, {} as ExSocketInterface);
|
||||
|
||||
const user2 = new User(
|
||||
2,
|
||||
"test",
|
||||
"10.0.0.2",
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
moving: false,
|
||||
direction: "down",
|
||||
},
|
||||
false,
|
||||
positionNotifier,
|
||||
{} as UserSocket,
|
||||
[],
|
||||
null,
|
||||
"foo",
|
||||
[]
|
||||
);
|
||||
const user2 = new User(2, 'test', {
|
||||
x: 0,
|
||||
y: 0,
|
||||
moving: false,
|
||||
direction: 'down'
|
||||
}, false, positionNotifier, {} as ExSocketInterface);
|
||||
|
||||
const listener = {} as ZoneSocket;
|
||||
positionNotifier.addZoneListener(listener, 0, 0);
|
||||
positionNotifier.addZoneListener(listener, 0, 1);
|
||||
positionNotifier.addZoneListener(listener, 1, 1);
|
||||
positionNotifier.addZoneListener(listener, 1, 0);
|
||||
/*let newUsers = positionNotifier.setViewport(user1, {
|
||||
let newUsers = positionNotifier.setViewport(user1, {
|
||||
left: 200,
|
||||
right: 600,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
positionNotifier.enter(user1);
|
||||
positionNotifier.enter(user2);
|
||||
});
|
||||
|
||||
//expect(newUsers.length).toBe(2);
|
||||
expect(newUsers.length).toBe(2);
|
||||
expect(enterTriggered).toBe(true);
|
||||
enterTriggered = false;
|
||||
|
||||
//positionNotifier.updatePosition(user2, {x:500, y:500}, {x:0, y: 0})
|
||||
user2.setPosition({ x: 500, y: 500, direction: "down", moving: false });
|
||||
user2.setPosition({x: 500, y: 500, direction: 'down', moving: false});
|
||||
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(true);
|
||||
enterTriggered = false;
|
||||
leaveTriggered = false;
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(true);
|
||||
moveTriggered = false;
|
||||
|
||||
// Add a listener, but the user in not in this zone.
|
||||
positionNotifier.addZoneListener(listener, 10, 10);
|
||||
|
||||
/*positionNotifier.setViewport(user1, {
|
||||
// Move the viewport but the user stays inside.
|
||||
positionNotifier.setViewport(user1, {
|
||||
left: 201,
|
||||
right: 601,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
});
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
|
||||
// Stop listening to zone
|
||||
positionNotifier.removeZoneListener(listener, 1, 1);
|
||||
// Move the viewport out of the user.
|
||||
/*positionNotifier.setViewport(user1, {
|
||||
positionNotifier.setViewport(user1, {
|
||||
left: 901,
|
||||
right: 1001,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
});
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(true);
|
||||
leaveTriggered = false;
|
||||
|
||||
// Move the viewport back on the user.
|
||||
positionNotifier.addZoneListener(listener, 1, 1);
|
||||
/*newUsers = positionNotifier.setViewport(user1, {
|
||||
newUsers = positionNotifier.setViewport(user1, {
|
||||
left: 200,
|
||||
right: 600,
|
||||
top: 100,
|
||||
bottom: 500
|
||||
});*/
|
||||
});
|
||||
|
||||
expect(enterTriggered).toBe(false);
|
||||
expect(enterTriggered).toBe(true);
|
||||
expect(moveTriggered).toBe(false);
|
||||
expect(leaveTriggered).toBe(false);
|
||||
enterTriggered = false;
|
||||
//expect(newUsers.length).toBe(2);
|
||||
expect(newUsers.length).toBe(2);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
19
back/tests/RoomIdentifierTest.ts
Normal file
19
back/tests/RoomIdentifierTest.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
|
||||
|
||||
describe("RoomIdentifier", () => {
|
||||
it("should flag public id as anonymous", () => {
|
||||
expect(isRoomAnonymous('_/global/test')).toBe(true);
|
||||
});
|
||||
it("should flag public id as not anonymous", () => {
|
||||
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
|
||||
});
|
||||
it("should extract roomSlug from public ID", () => {
|
||||
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
|
||||
});
|
||||
it("should extract correct from private ID", () => {
|
||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
|
||||
expect(organizationSlug).toBe('afup');
|
||||
expect(worldSlug).toBe('afup2020');
|
||||
expect(roomSlug).toBe('1floor');
|
||||
});
|
||||
})
|
|
@ -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,7 +3,7 @@
|
|||
"experimentalDecorators": true,
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"downlevelIteration": true,
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
|
|
2986
back/yarn.lock
2986
back/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,6 @@ import {RoomConnection} from "../front/src/Connexion/RoomConnection";
|
|||
import {connectionManager} from "../front/src/Connexion/ConnectionManager";
|
||||
import * as WebSocket from "ws"
|
||||
|
||||
let userMovedCount = 0;
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
@ -13,7 +12,8 @@ RoomConnection.setWebsocketFactory((url: string) => {
|
|||
});
|
||||
|
||||
async function startOneUser(): Promise<void> {
|
||||
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
||||
await connectionManager.anonymousLogin(true);
|
||||
const connection = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
||||
{
|
||||
x: 783,
|
||||
y: 170
|
||||
|
@ -22,13 +22,7 @@ async function startOneUser(): Promise<void> {
|
|||
bottom: 200,
|
||||
left: 500,
|
||||
right: 800
|
||||
}, null);
|
||||
|
||||
const connection = onConnect.connection;
|
||||
|
||||
connection.onUserMoved(() => {
|
||||
userMovedCount++;
|
||||
})
|
||||
});
|
||||
|
||||
console.log(connection.getUserId());
|
||||
|
||||
|
@ -57,15 +51,10 @@ async function startOneUser(): Promise<void> {
|
|||
(async () => {
|
||||
connectionManager.initBenchmark();
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (let userNo = 0; userNo < 160; userNo++) {
|
||||
const promise = startOneUser();
|
||||
promises.push(promise);
|
||||
startOneUser();
|
||||
// Wait 0.5s between adding users
|
||||
await sleep(125);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
console.log('User moved count: '+userMovedCount);
|
||||
})();
|
||||
|
|
24
benchmark/package-lock.json
generated
24
benchmark/package-lock.json
generated
|
@ -209,9 +209,9 @@
|
|||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
||||
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
|
@ -230,9 +230,9 @@
|
|||
}
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "2.1.0",
|
||||
|
@ -429,9 +429,9 @@
|
|||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
|
||||
},
|
||||
"path-type": {
|
||||
"version": "1.1.0",
|
||||
|
@ -688,9 +688,9 @@
|
|||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
|
||||
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"@types/ws": "^7.2.6",
|
||||
"ts-node-dev": "^1.0.0-pre.62",
|
||||
"typescript": "^4.0.2",
|
||||
"ws": "^7.4.6"
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
|
|
@ -148,8 +148,8 @@ get-stdin@^4.0.1:
|
|||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||
|
||||
glob-parent@~5.1.0:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
|
@ -169,8 +169,8 @@ graceful-fs@^4.1.2:
|
|||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
|
||||
indent-string@^2.1.0:
|
||||
version "2.1.0"
|
||||
|
@ -315,8 +315,8 @@ path-is-absolute@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
|
||||
path-parse@^1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
|
||||
|
||||
path-type@^1.0.0:
|
||||
version "1.1.0"
|
||||
|
@ -515,9 +515,9 @@ wrappy@1:
|
|||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
||||
ws@^7.4.6:
|
||||
version "7.4.6"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
|
||||
ws@^7.3.1:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
||||
|
||||
xtend@^4.0.0:
|
||||
version "4.0.2"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue