Merge branch 'develop' into exitTriggerAction

# Conflicts:
#	front/src/Phaser/Game/GameScene.ts
This commit is contained in:
Gregoire Parant 2021-02-17 19:28:41 +01:00
commit 83fc7d0cc0
88 changed files with 1382 additions and 1996 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
**/node_modules/**
**/Dockerfile

View File

@ -6,3 +6,7 @@ JITSI_ISS=
SECRET_JITSI_KEY= SECRET_JITSI_KEY=
ADMIN_API_TOKEN=123 ADMIN_API_TOKEN=123
START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.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=

View File

@ -150,6 +150,7 @@ jobs:
JITSI_ISS: ${{ secrets.JITSI_ISS }} JITSI_ISS: ${{ secrets.JITSI_ISS }}
JITSI_URL: ${{ secrets.JITSI_URL }} JITSI_URL: ${{ secrets.JITSI_URL }}
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }} SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
with: with:
namespace: workadventure-${{ env.GITHUB_REF_SLUG }} namespace: workadventure-${{ env.GITHUB_REF_SLUG }}
@ -159,34 +160,5 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
msg: Environment deployed at https://${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com msg: Environment deployed at https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
check_for_duplicate_msg: true 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"

View File

@ -6,8 +6,6 @@ Demo here : [https://workadventu.re/](https://workadventu.re/).
# Work Adventure # Work Adventure
## 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 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. 16-bit video game.
@ -15,7 +13,7 @@ In Work Adventure, you can move around your office and talk to your colleagues (
triggered when you move next to a colleague). triggered when you move next to a colleague).
## Getting started ## Setting up a development environment
Install Docker. Install Docker.
@ -101,5 +99,7 @@ Vagrant destroy
* `Vagrant halt`: stop your VM Vagrant. * `Vagrant halt`: stop your VM Vagrant.
* `Vagrant destroy`: delete your VM Vagrant. * `Vagrant destroy`: delete your VM Vagrant.
## Features developed ## Setting up a production environment
You have more details of features developed in back [README.md](./back/README.md).
The way you set up your production environment will highly depend on your servers.
We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) directory.

View File

@ -1,16 +1,26 @@
FROM thecodingmachine/workadventure-back-base:latest as builder # protobuf build
WORKDIR /var/www/messages FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
COPY --chown=docker:docker messages . WORKDIR /usr/src
COPY messages .
RUN yarn install && yarn proto RUN yarn install && yarn proto
FROM thecodingmachine/nodejs:12 # typescript build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
COPY --chown=docker:docker back . WORKDIR /usr/src
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated COPY back/yarn.lock back/package.json ./
RUN yarn install RUN yarn install
COPY back .
COPY --from=builder /usr/src/generated src/Messages/generated
ENV NODE_ENV=production ENV NODE_ENV=production
RUN yarn run tsc RUN yarn run tsc
CMD ["yarn", "run", "runprod"] # final production image
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76
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"]

View File

@ -1,61 +0,0 @@
# 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)

View File

@ -1,4 +1,3 @@
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; 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 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 ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
@ -12,9 +11,9 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080; const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080;
const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051; const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051;
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 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 const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || '';
export { export {
SECRET_KEY,
MINIMUM_DISTANCE, MINIMUM_DISTANCE,
ADMIN_API_URL, ADMIN_API_URL,
ADMIN_API_TOKEN, ADMIN_API_TOKEN,

View File

@ -100,11 +100,12 @@ class AdminApi {
return res.data; return res.data;
} }
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) { reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) {
return Axios.post(`${ADMIN_API_URL}/api/report`, { return Axios.post(`${ADMIN_API_URL}/api/report`, {
reportedUserUuid, reportedUserUuid,
reportedUserComment, reportedUserComment,
reporterUserUuid, reporterUserUuid,
reportWorldSlug,
}, },
{ {
headers: {"Authorization": `${ADMIN_API_TOKEN}`} headers: {"Authorization": `${ADMIN_API_TOKEN}`}

View File

@ -28,7 +28,13 @@ import {User, UserSocket} from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {Group} from "../Model/Group"; import {Group} from "../Model/Group";
import {cpuTracker} from "./CpuTracker"; import {cpuTracker} from "./CpuTracker";
import {GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; import {
GROUP_RADIUS,
JITSI_ISS,
MINIMUM_DISTANCE,
SECRET_JITSI_KEY,
TURN_STATIC_AUTH_SECRET
} from "../Enum/EnvironmentVariable";
import {Movable} from "../Model/Movable"; import {Movable} from "../Model/Movable";
import {PositionInterface} from "../Model/PositionInterface"; import {PositionInterface} from "../Model/PositionInterface";
import {adminApi, CharacterTexture} from "./AdminApi"; import {adminApi, CharacterTexture} from "./AdminApi";
@ -40,6 +46,8 @@ import {ZoneSocket} from "../RoomManager";
import {Zone} from "_Model/Zone"; import {Zone} from "_Model/Zone";
import Debug from "debug"; import Debug from "debug";
import {Admin} from "_Model/Admin"; import {Admin} from "_Model/Admin";
import crypto from "crypto";
const debug = Debug('sockermanager'); const debug = Debug('sockermanager');
@ -275,6 +283,12 @@ export class SocketManager {
const webrtcSignalToClient = new WebRtcSignalToClientMessage(); const webrtcSignalToClient = new WebRtcSignalToClientMessage();
webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal()); webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password);
}
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
@ -295,6 +309,12 @@ export class SocketManager {
const webrtcSignalToClient = new WebRtcSignalToClientMessage(); const webrtcSignalToClient = new WebRtcSignalToClientMessage();
webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal()); webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password);
}
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
@ -487,6 +507,11 @@ export class SocketManager {
webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setName(otherUser.name);
webrtcStartMessage1.setInitiator(true); webrtcStartMessage1.setInitiator(true);
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET);
webrtcStartMessage1.setWebrtcusername(username);
webrtcStartMessage1.setWebrtcpassword(password);
}
const serverToClientMessage1 = new ServerToClientMessage(); const serverToClientMessage1 = new ServerToClientMessage();
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
@ -500,6 +525,11 @@ export class SocketManager {
webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setName(user.name);
webrtcStartMessage2.setInitiator(false); webrtcStartMessage2.setInitiator(false);
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
webrtcStartMessage2.setWebrtcusername(username);
webrtcStartMessage2.setWebrtcpassword(password);
}
const serverToClientMessage2 = new ServerToClientMessage(); const serverToClientMessage2 = new ServerToClientMessage();
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
@ -512,6 +542,25 @@ export class SocketManager {
} }
} }
/**
* Computes a unique user/password for the TURN server, using a shared secret between the WorkAdventure API server
* and the Coturn server.
* The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
*/
private getTURNCredentials(name: string, secret: string): {username: string, password: string} {
const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours
const username = [unixTimeStamp, name].join(':');
const hmac = crypto.createHmac('sha1', secret);
hmac.setEncoding('base64');
hmac.write(username);
hmac.end();
const password = hmac.read();
return {
username: username,
password: password
};
}
//disconnect user //disconnect user
private disConnectedUser(user: User, group: Group) { private disConnectedUser(user: User, group: Group) {
// Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection // Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection

View File

@ -0,0 +1,20 @@
# The base domain
DOMAIN=workadventure.localhost
DEBUG_MODE=false
JITSI_URL=meet.jit.si
# If your Jitsi environment has authentication set up, you MUST set JITSI_PRIVATE_MODE to "true" and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret
JITSI_PRIVATE_MODE=false
JITSI_ISS=
SECRET_JITSI_KEY=
# URL of the TURN server (needed to "punch a hole" through some networks for P2P connections)
TURN_SERVER=
TURN_USER=
TURN_PASSWORD=
# The URL used by default, in the form: "/_/global/map/url.json"
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json
# The email address used by Let's encrypt to send renewal warnings (compulsory)
ACME_EMAIL=

View File

@ -0,0 +1,100 @@
version: "3.3"
services:
reverse-proxy:
image: traefik:v2.3
command:
- --log.level=WARN
#- --api.insecure=true
- --providers.docker
- --entryPoints.web.address=:80
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entryPoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.email=d.negrier@thecodingmachine.com
- --certificatesresolvers.myresolver.acme.storage=/acme.json
# used during the challenge
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
# The Web UI (enabled by --api.insecure=true)
#- "8080:8080"
depends_on:
- pusher
- front
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./acme.json:/acme.json
restart: unless-stopped
front:
build:
context: ../..
dockerfile: front/Dockerfile
#image: thecodingmachine/workadventure-front:master
environment:
DEBUG_MODE: "$DEBUG_MODE"
JITSI_URL: $JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
API_URL: pusher.${DOMAIN}
TURN_SERVER: "${TURN_SERVER}"
TURN_USER: "${TURN_USER}"
TURN_PASSWORD: "${TURN_PASSWORD}"
START_ROOM_URL: "${START_ROOM_URL}"
labels:
- "traefik.http.routers.front.rule=Host(`play.${DOMAIN}`)"
- "traefik.http.routers.front.entryPoints=web,traefik"
- "traefik.http.services.front.loadbalancer.server.port=80"
- "traefik.http.routers.front-ssl.rule=Host(`play.${DOMAIN}`)"
- "traefik.http.routers.front-ssl.entryPoints=websecure"
- "traefik.http.routers.front-ssl.tls=true"
- "traefik.http.routers.front-ssl.service=front"
- "traefik.http.routers.front-ssl.tls.certresolver=myresolver"
restart: unless-stopped
pusher:
build:
context: ../..
dockerfile: pusher/Dockerfile
#image: thecodingmachine/workadventure-pusher:master
command: yarn run runprod
environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey
API_URL: back:50051
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
labels:
- "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)"
- "traefik.http.routers.pusher.entryPoints=web,traefik"
- "traefik.http.services.pusher.loadbalancer.server.port=8080"
- "traefik.http.routers.pusher-ssl.rule=Host(`pusher.${DOMAIN}`)"
- "traefik.http.routers.pusher-ssl.entryPoints=websecure"
- "traefik.http.routers.pusher-ssl.tls=true"
- "traefik.http.routers.pusher-ssl.service=pusher"
- "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver"
restart: unless-stopped
back:
build:
context: ../..
dockerfile: back/Dockerfile
#image: thecodingmachine/workadventure-back:master
command: yarn run runprod
environment:
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
ADMIN_API_URL: "$ADMIN_API_URL"
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
labels:
- "traefik.http.routers.back.rule=Host(`api.${DOMAIN}`)"
- "traefik.http.routers.back.entryPoints=web"
- "traefik.http.services.back.loadbalancer.server.port=8080"
- "traefik.http.routers.back-ssl.rule=Host(`api.${DOMAIN}`)"
- "traefik.http.routers.back-ssl.entryPoints=websecure"
- "traefik.http.routers.back-ssl.tls=true"
- "traefik.http.routers.back-ssl.service=back"
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
restart: unless-stopped

View File

@ -3,7 +3,8 @@
local namespace = env.GITHUB_REF_SLUG, local namespace = env.GITHUB_REF_SLUG,
local tag = namespace, local tag = namespace,
local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com", local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com",
local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://"+url else null, // develop branch does not use admin because of issue with SSL certificate of admin as of now.
local adminUrl = if namespace == "master" /*|| namespace == "develop"*/ || std.startsWith(namespace, "admin") then "https://"+url else null,
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
"version": "1.0", "version": "1.0",
"containers": { "containers": {
@ -21,6 +22,7 @@
"JITSI_ISS": env.JITSI_ISS, "JITSI_ISS": env.JITSI_ISS,
"JITSI_URL": env.JITSI_URL, "JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
} + if adminUrl != null then { } + if adminUrl != null then {
"ADMIN_API_URL": adminUrl, "ADMIN_API_URL": adminUrl,
} else {} } else {}
@ -39,6 +41,7 @@
"JITSI_ISS": env.JITSI_ISS, "JITSI_ISS": env.JITSI_ISS,
"JITSI_URL": env.JITSI_URL, "JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
} + if adminUrl != null then { } + if adminUrl != null then {
"ADMIN_API_URL": adminUrl, "ADMIN_API_URL": adminUrl,
} else {} } else {}
@ -79,6 +82,7 @@
"TURN_USER": "workadventure", "TURN_USER": "workadventure",
"TURN_PASSWORD": "WorkAdventure123", "TURN_PASSWORD": "WorkAdventure123",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false", "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
"START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json"
//"GA_TRACKING_ID": "UA-10196481-11" //"GA_TRACKING_ID": "UA-10196481-11"
} }
}, },

View File

@ -1,20 +0,0 @@
version: '3'
services:
wait_app:
image: dadarek/wait-for-dependencies
depends_on:
- reverse-proxy
command: front:8080
cypress:
# the Docker image to use from https://github.com/cypress-io/cypress-docker-images
image: "cypress/included:3.8.3"
depends_on:
- reverse-proxy
environment:
# pass base url to test pointing at the web application
- CYPRESS_baseUrl=http://front:8080
working_dir: /e2e
volumes:
- ./e2e/:/e2e

View File

@ -31,9 +31,12 @@ services:
ADMIN_URL: workadventure.localhost ADMIN_URL: workadventure.localhost
STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install STARTUP_COMMAND_2: yarn install
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443" STUN_SERVER: "stun:stun.l.google.com:19302"
TURN_USER: workadventure TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349"
TURN_PASSWORD: WorkAdventure123 # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
TURN_USER: ""
TURN_PASSWORD: ""
START_ROOM_URL: "$START_ROOM_URL" START_ROOM_URL: "$START_ROOM_URL"
command: yarn run start command: yarn run start
volumes: volumes:
@ -108,6 +111,7 @@ services:
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
JITSI_URL: $JITSI_URL JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS JITSI_ISS: $JITSI_ISS
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
volumes: volumes:
- ./back:/usr/src/app - ./back:/usr/src/app
labels: labels:
@ -149,3 +153,28 @@ services:
- ./back:/usr/src/back - ./back:/usr/src/back
- ./front:/usr/src/front - ./front:/usr/src/front
- ./pusher:/usr/src/pusher - ./pusher:/usr/src/pusher
# coturn:
# image: coturn/coturn:4.5.2
# command:
# - turnserver
# #- -c=/etc/coturn/turnserver.conf
# - --log-file=stdout
# - --external-ip=$$(detect-external-ip)
# - --listening-port=3478
# - --min-port=10000
# - --max-port=10010
# - --tls-listening-port=5349
# - --listening-ip=0.0.0.0
# - --realm=coturn.workadventure.localhost
# - --server-name=coturn.workadventure.localhost
# - --lt-cred-mech
# # Enable Coturn "REST API" to validate temporary passwords.
# #- --use-auth-secret
# #- --static-auth-secret=SomeStaticAuthSecret
# #- --userdb=/var/lib/turn/turndb
# - --user=workadventure:WorkAdventure123
# # use real-valid certificate/privatekey files
# #- --cert=/root/letsencrypt/fullchain.pem
# #- --pkey=/root/letsencrypt/privkey.pem
# network_mode: host

3
e2e/.gitignore vendored
View File

@ -1,3 +0,0 @@
screenshots/
videos/
node_modules/

View File

@ -1,36 +0,0 @@
# Testing with cypress
This project use [cypress](https://www.cypress.io/) to do functional testing of the website.
Unfortunately we cannot integrate it with docker-compose for the moment, so you will need to install some packages locally on your pc.
## Getting Started
You will need to install theses dependancies on linux (don't know about mac):
```bash
sudo apt update
sudo apt install libgtk2.0-0 libgtk-3-0 libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
```
Cypress can be installed locally in the e2e directory
```bash
cd e2e
npm install
```
How to use:
```bash
npm run cy:run
npm run cy:open
```
The first command will run all tests in the terminal, while the second will open the interactive task runner which allow you to easily manage the test workflow
[More details here](https://docs.cypress.io/guides/getting-started/testing-your-app.html#Step-1-Start-your-server)
## How to test a game
Cypress cannot "see" and so cannot directly manipulate the canva created by Phaser.
This means we have to do workarounds such as exposing core objects in the window so that cypress can manipulate them or doing console that cypress can catch.

View File

@ -1,7 +0,0 @@
{
"baseUrl": "http://workadventure.localhost",
"video": false,
"defaultCommandTimeout": 20000,
"pluginsFile": false,
"supportFile": false
}

View File

@ -1,25 +0,0 @@
Cypress.on('window:before:load', (win) => {
// because this is called before any scripts
// have loaded - the ga function is undefined
// so we need to create it.
win.cypressAsserter = cy.stub().as('ca')
})
describe('WorkAdventureGame', () => {
beforeEach(() => {
cy.visit('/', {
onBeforeLoad (win) {
cy.spy(win.console, 'log').as('console.log')
},
})
});
it('loads', () => {
cy.get('@console.log').should('be.calledWith', 'Started the game')
cy.get('@console.log').should('be.calledWith', 'Preloading')
cy.get('@console.log').should('be.calledWith', 'Preloading done')
cy.get('@console.log').should('be.calledWith', 'startInit')
cy.get('@console.log').should('be.calledWith', 'startInit done')
});
});

1406
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
{
"dependencies": {
"cypress": "^3.8.3"
},
"scripts": {
"cy:run": "cypress run",
"cy:open": "cypress open"
}
}

1
front/dist/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
index.html

View File

@ -15,6 +15,14 @@
#gameMenu section { #gameMenu section {
margin: 10px; margin: 10px;
} }
section#socialLinks{
position: absolute;
margin-bottom: 0;
}
section#socialLinks img{
width: 32px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
</style> </style>
<div id="gameMenu" hidden> <div id="gameMenu" hidden>
@ -38,5 +46,9 @@
<section id="adminConsoleSection" hidden> <section id="adminConsoleSection" hidden>
<button id="adminConsoleButton">Admin console</button> <button id="adminConsoleButton">Admin console</button>
</section> </section>
<section id="socialLinks" hidden>
<a class="not-button" href="https://www.facebook.com/workadventurebytcm" target="_blank"><img class="not-button" src="/resources/objects/facebook-icon.png"/></a>
<a class="not-button" href="https://twitter.com/Workadventure_" target="_blank"><img class="not-button" src="/resources/objects/twitter-icon.png"/></a>
</section>
</main> </main>
</div> </div>

View File

@ -0,0 +1,122 @@
<style>
*{
font-family: 'Open Sans', sans-serif;
cursor: url('/resources/logos/cursor_normal.png'), auto;
}
* a, button, input{
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
#gameReport {
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
margin: 2px auto 0;
width: 298px;
}
#gameReport h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#gameReport h3 {
margin: 0;
}
#gameReport textarea {
font-size: 70%;
background: linear-gradient(top, #d6d7d7, #dee0e0);
border: 1px solid #a1a3a3;
border-radius: 4px;
box-shadow: 0 1px #fff;
box-sizing: border-box;
color: #696969;
height: 100px;
transition: box-shadow 0.3s;
width: 100%;
}
#gameReport section {
margin: 10px;
}
#gameReport section.action{
text-align: center;
margin: 0;
}
#gameReport button {
margin-top: 10px;
font-size: 60%;
background-color: #dc3545;
color: white;
border-radius: 7px;
padding: 3px 10px 3px 10px;
}
#gameReport button#gameReportFormCancel {
background-color: #c7c7c700;
color: #292929;
display: block;
float: right;
}
#gameReport section a{
text-align: center;
font-size: 12px;
margin: 0 6px;
color: black;
}
#gameReport section h6,
#gameReport section h5{
margin: 1px;
}
#gameReport section.text-center{
text-align: center;
}
#gameReport p{
font-size: 8px;
margin: 3px 0 0 0;
}
#gameReport form p{
margin: 0px 70px;
}
#gameReport section p.err{
color: red;
display: none;
}
#gameReport section p.info{
display: none;
}
</style>
<main id="gameReport" hidden>
<section>
<button id="gameReportFormCancel">X</button>
<h1>Moderate <span id="nameReported"></span></h1>
<p id="askActionP">What action do you want to take?</p>
</section>
<section>
<h3>Block: </h3>
<p>Block any communication from and to this user. This can be reverted.</p>
<section class="action">
<button id="toggleBlockButton">Block this user</button>
</section>
</section>
<section id="reportSection">
<h3>Report: </h3>
<p>Send a report message to the administrators of this room. They may later ban this user.</p>
<form>
<section>
<h6>Your message: </h6>
<textarea type="text" name="report" id="gameReportInput"></textarea>
<p class="err" id="gameReportErr"></p>
</section>
<section class="action">
<button type="submit" id="gameReportFormSubmit">Report this user</button>
</section>
</form>
</section>
</main>

View File

@ -14,9 +14,6 @@
width: 298px; width: 298px;
height: 150px; height: 150px;
} }
#gameShare .cautiousText {
font-size: 50%;
}
#gameShare h1 { #gameShare h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0); background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf; border-bottom: 1px solid #a6abaf;

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg2985" version="1.1" inkscape:version="0.48.4 r9939" width="485.33627" height="485.33627" sodipodi:docname="600px-France_road_sign_B1j.svg[1].png">
<metadata id="metadata2991">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs2989"/>
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1272" inkscape:window-height="745" id="namedview2987" showgrid="false" inkscape:snap-global="true" inkscape:snap-grids="true" inkscape:snap-bbox="true" inkscape:bbox-paths="true" inkscape:bbox-nodes="true" inkscape:snap-bbox-edge-midpoints="true" inkscape:snap-bbox-midpoints="true" inkscape:object-paths="true" inkscape:snap-intersection-paths="true" inkscape:object-nodes="true" inkscape:snap-smooth-nodes="true" inkscape:snap-midpoints="true" inkscape:snap-object-midpoints="true" inkscape:snap-center="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:zoom="0.59970176" inkscape:cx="390.56499" inkscape:cy="244.34365" inkscape:window-x="86" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1">
<inkscape:grid type="xygrid" id="grid2995" empspacing="5" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="-57.33186px" originy="-57.33186px"/>
</sodipodi:namedview>
<g inkscape:groupmode="layer" id="layer1" inkscape:label="1" style="display:inline" transform="translate(-57.33186,-57.33186)">
<path sodipodi:type="arc" style="color:#000000;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path2997" sodipodi:cx="300" sodipodi:cy="300" sodipodi:rx="240" sodipodi:ry="240" d="M 540,300 C 540,432.54834 432.54834,540 300,540 167.45166,540 60,432.54834 60,300 60,167.45166 167.45166,60 300,60 432.54834,60 540,167.45166 540,300 z" transform="matrix(1.0058783,0,0,1.0058783,-1.76349,-1.76349)"/>
<path sodipodi:type="arc" style="color:#000000;fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4005" sodipodi:cx="304.75" sodipodi:cy="214.75" sodipodi:rx="44.75" sodipodi:ry="44.75" d="m 349.5,214.75 c 0,24.71474 -20.03526,44.75 -44.75,44.75 -24.71474,0 -44.75,-20.03526 -44.75,-44.75 0,-24.71474 20.03526,-44.75 44.75,-44.75 24.71474,0 44.75,20.03526 44.75,44.75 z" transform="matrix(5.1364411,0,0,5.1364411,-1265.3304,-803.05073)"/>
<rect style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.5;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="rect4001" width="345" height="80.599998" x="127.5" y="259.70001"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
front/dist/resources/logos/cancel.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
front/dist/resources/logos/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56.48 56.48"><defs><style>.cls-1{fill:#e76e54;}.cls-2{fill:#fff;}</style></defs><path class="cls-1" d="M39.94,512H16.54L0,495.46v-23.4l16.54-16.54h23.4l16.54,16.54v23.4Z" transform="translate(0 -455.52)"/><path class="cls-2" d="M33.54,485.52H23l-1.77-21.18H35.3Z" transform="translate(0 -455.52)"/><path class="cls-2" d="M23,492.58H33.54v10.59H23Z" transform="translate(0 -455.52)"/></svg>

After

Width:  |  Height:  |  Size: 477 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -39,6 +39,7 @@ body .message-info.warning{
position: relative; position: relative;
transition: all 0.2s ease; transition: all 0.2s ease;
background-color: #00000099; background-color: #00000099;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
} }
.video-container i{ .video-container i{
position: absolute; position: absolute;
@ -53,25 +54,71 @@ body .message-info.warning{
font-size: 28px; font-size: 28px;
color: white; color: white;
} }
.video-container img.active{
display: block;
}
.video-container img{ .video-container img{
position: absolute; position: absolute;
display: none; display: none;
width: 15px; width: 25px;
height: 15px; height: 25px;
background: #d93025;
border-radius: 48px;
left: 5px; left: 5px;
bottom: 5px; bottom: 5px;
padding: 10px; padding: 10px;
z-index: 2; z-index: 2;
} }
.video-container img.block-logo {
left: 30%;
bottom: 15%;
width: 150px;
height: 150px;
}
.video-container img.report{ .video-container button.report{
display: block;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
background: none;
background-color: rgba(0, 0, 0, 0);
border: none;
background-color: black;
border-radius: 15px;
position: absolute;
width: 0px;
height: 35px;
right: 5px; right: 5px;
left: auto; bottom: 5px;
padding: 0px;
overflow: hidden;
z-index: 2;
transition: all .5s ease;
}
.video-container:hover button.report{
width: 35px;
padding: 10px;
}
.video-container button.report:hover {
width: 150px;
}
.video-container button.report img{
position: absolute;
display: block;
bottom: 5px;
left: 5px;
margin: 0;
padding: 0;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
.video-container button.report span{
position: absolute;
bottom: 8px;
left: 36px;
color: white;
font-size: 16px;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
}
.video-container img.active {
display: block !important;
} }
.video-container video{ .video-container video{
@ -150,10 +197,7 @@ video#myCamVideo{
transition: all .2s; transition: all .2s;
right: 224px; right: 224px;
} }
/*.btn-call{
transition: all .1s;
left: 0px;
}*/
.btn-cam-action div img{ .btn-cam-action div img{
height: 22px; height: 22px;
width: 30px; width: 30px;
@ -352,6 +396,7 @@ body {
#cowebsite { #cowebsite {
position: fixed; position: fixed;
transition: transform 0.5s; transition: transform 0.5s;
background-color: white;
} }
#cowebsite.loading { #cowebsite.loading {
background-color: gray; background-color: gray;
@ -1078,7 +1123,7 @@ div.modal-report-user{
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
} }
.discussion .messages .message p.a{ .discussion .messages .message p a{
color: white; color: white;
} }

View File

@ -26,9 +26,10 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"generic-type-guard": "^3.2.0", "generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"phaser": "^3.52.0", "phaser": "3.24.1",
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "^1.3.7", "quill": "^1.3.7",
"rxjs": "^6.6.3",
"simple-peer": "^9.6.2", "simple-peer": "^9.6.2",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"webpack-require-http": "^0.4.3" "webpack-require-http": "^0.4.3"

View File

@ -332,7 +332,7 @@ export class ConsoleGlobalMessageManager {
} }
active(){ active(){
this.userInputManager.clearAllInputKeyboard(); this.userInputManager.clearAllKeys();
this.divMainConsole.style.top = '0'; this.divMainConsole.style.top = '0';
this.activeConsole = true; this.activeConsole = true;
} }

View File

@ -1,5 +1,8 @@
import {RoomConnection} from "../Connexion/RoomConnection"; import {RoomConnection} from "../Connexion/RoomConnection";
import * as TypeMessages from "./TypeMessage"; import * as TypeMessages from "./TypeMessage";
import List = Phaser.Structs.List;
import {UpdatedLocalStreamCallback} from "../WebRtc/MediaManager";
import {Banned} from "./TypeMessage";
export interface TypeMessageInterface { export interface TypeMessageInterface {
showMessage(message: string): void; showMessage(message: string): void;
@ -8,6 +11,7 @@ export interface TypeMessageInterface {
export class UserMessageManager { export class UserMessageManager {
typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>(); typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
receiveBannedMessageListener: Set<Function> = new Set<UpdatedLocalStreamCallback>();
constructor(private Connection: RoomConnection) { constructor(private Connection: RoomConnection) {
const valueTypeMessageTab = Object.values(TypeMessages); const valueTypeMessageTab = Object.values(TypeMessages);
@ -21,7 +25,14 @@ export class UserMessageManager {
initialise() { initialise() {
//receive signal to show message //receive signal to show message
this.Connection.receiveUserMessage((type: string, message: string) => { this.Connection.receiveUserMessage((type: string, message: string) => {
this.showMessage(type, message); const typeMessage = this.showMessage(type, message);
//listener on banned receive message
if(typeMessage instanceof Banned) {
for (const callback of this.receiveBannedMessageListener) {
callback();
}
}
}); });
} }
@ -32,5 +43,10 @@ export class UserMessageManager {
return; return;
} }
classTypeMessage.showMessage(message); classTypeMessage.showMessage(message);
return classTypeMessage;
}
setReceiveBanListener(callback: Function){
this.receiveBannedMessageListener.add(callback);
} }
} }

View File

@ -96,7 +96,9 @@ export interface WebRtcSignalSentMessageInterface {
export interface WebRtcSignalReceivedMessageInterface { export interface WebRtcSignalReceivedMessageInterface {
userId: number, userId: number,
signal: SignalData signal: SignalData,
webRtcUser: string | undefined,
webRtcPassword: string | undefined
} }
export interface StartMapInterface { export interface StartMapInterface {

View File

@ -427,7 +427,9 @@ export class RoomConnection implements RoomConnection {
callback({ callback({
userId: message.getUserid(), userId: message.getUserid(),
name: message.getName(), name: message.getName(),
initiator: message.getInitiator() initiator: message.getInitiator(),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
}); });
}); });
} }
@ -436,7 +438,9 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_SIGNAL, (message: WebRtcSignalToClientMessage) => { this.onMessage(EventMessage.WEBRTC_SIGNAL, (message: WebRtcSignalToClientMessage) => {
callback({ callback({
userId: message.getUserid(), userId: message.getUserid(),
signal: JSON.parse(message.getSignal()) signal: JSON.parse(message.getSignal()),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
}); });
}); });
} }
@ -445,7 +449,9 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, (message: WebRtcSignalToClientMessage) => { this.onMessage(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, (message: WebRtcSignalToClientMessage) => {
callback({ callback({
userId: message.getUserid(), userId: message.getUserid(),
signal: JSON.parse(message.getSignal()) signal: JSON.parse(message.getSignal()),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
}); });
}); });
} }
@ -464,7 +470,8 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public getUserId(): number|null { public getUserId(): number {
if (this.userId === null) throw 'UserId cannot be null!'
return this.userId; return this.userId;
} }

View File

@ -1,36 +0,0 @@
declare let window:WindowWithCypressAsserter;
interface WindowWithCypressAsserter extends Window {
cypressAsserter: CypressAsserter;
}
//this class is used to communicate with cypress, our e2e testing client
//Since cypress cannot manipulate canvas, we notified it with console logs
class CypressAsserter {
constructor() {
window.cypressAsserter = this
}
gameStarted() {
console.log('Started the game')
}
preloadStarted() {
console.log('Preloading')
}
preloadFinished() {
console.log('Preloading done')
}
initStarted() {
console.log('startInit')
}
initFinished() {
console.log('startInit done')
}
}
export const cypressAsserter = new CypressAsserter()

View File

@ -3,9 +3,10 @@ const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.wo
const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "pusher.workadventure.localhost"); const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "pusher.workadventure.localhost");
const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.workadventure.localhost'); const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.workadventure.localhost');
const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "workadventure.localhost"); const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "workadventure.localhost");
const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca"; const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com'; const TURN_SERVER: string = process.env.TURN_SERVER || "";
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$'; const TURN_USER: string = process.env.TURN_USER || '';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || '';
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true"; const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
const RESOLUTION = 2; const RESOLUTION = 2;
@ -23,6 +24,7 @@ export {
ZOOM_LEVEL, ZOOM_LEVEL,
POSITION_DELAY, POSITION_DELAY,
MAX_EXTRAPOLATION_TIME, MAX_EXTRAPOLATION_TIME,
STUN_SERVER,
TURN_SERVER, TURN_SERVER,
TURN_USER, TURN_USER,
TURN_PASSWORD, TURN_PASSWORD,

View File

@ -1,14 +1,54 @@
import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig;
export const addLoader = (scene:Phaser.Scene): void => { const LogoNameIndex: string = 'logoLoading';
const loadingText = scene.add.text(scene.game.renderer.width / 2, 200, 'Loading'); const TextName: string = 'Loading...';
const LogoResource: string = 'resources/logos/logo.png';
const LogoFrame: ImageFrameConfig = {frameWidth: 307, frameHeight: 59};
export const addLoader = (scene: Phaser.Scene): void => {
// If there is nothing to load, do not display the loader.
if (scene.load.list.entries.length === 0) {
return;
}
let loadingText: Phaser.GameObjects.Text|null = null;
const loadingBarWidth: number = Math.floor(scene.game.renderer.width / 3);
const loadingBarHeight: number = 16;
const padding: number = 5;
const promiseLoadLogoTexture = new Promise<Phaser.GameObjects.Image>((res) => {
if(scene.load.textureManager.exists(LogoNameIndex)){
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex));
}else{
//add loading if logo image is not ready
loadingText = scene.add.text(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 50, TextName);
}
scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame);
scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => {
if(loadingText){
loadingText.destroy();
}
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex));
});
});
const progressContainer = scene.add.graphics();
const progress = scene.add.graphics(); const progress = scene.add.graphics();
progressContainer.fillStyle(0x444444, 0.8);
progressContainer.fillRect((scene.game.renderer.width - loadingBarWidth) / 2 - padding, scene.game.renderer.height / 2 + 50 - padding, loadingBarWidth + padding * 2, loadingBarHeight + padding * 2);
scene.load.on('progress', (value: number) => { scene.load.on('progress', (value: number) => {
progress.clear(); progress.clear();
progress.fillStyle(0xffffff, 1); progress.fillStyle(0xBBBBBB, 1);
progress.fillRect(0, 270, 800 * value, 60); progress.fillRect((scene.game.renderer.width - loadingBarWidth) / 2, scene.game.renderer.height / 2 + 50, loadingBarWidth * value, loadingBarHeight);
}); });
scene.load.on('complete', () => { scene.load.on('complete', () => {
if(loadingText){
loadingText.destroy(); loadingText.destroy();
}
promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => {
resLoadingImage.destroy();
});
progress.destroy(); progress.destroy();
progressContainer.destroy();
}); });
} }

View File

@ -3,14 +3,12 @@ import {discussionManager} from "../../WebRtc/DiscussionManager";
export const openChatIconName = 'openChatIcon'; export const openChatIconName = 'openChatIcon';
export class OpenChatIcon extends Phaser.GameObjects.Image { export class OpenChatIcon extends Phaser.GameObjects.Image {
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, openChatIconName); super(scene, x, y, openChatIconName, 3);
scene.add.existing(this); scene.add.existing(this);
this.setScrollFactor(0, 0); this.setScrollFactor(0, 0);
this.setOrigin(0, 1); this.setOrigin(0, 1);
this.displayWidth = 30;
this.displayHeight = 30;
this.setInteractive(); this.setInteractive();
this.setVisible(false) this.setVisible(false);
this.setDepth(99999); this.setDepth(99999);
this.on("pointerup", () => discussionManager.showDiscussionPart()); this.on("pointerup", () => discussionManager.showDiscussionPart());

View File

@ -6,7 +6,8 @@ export interface BodyResourceDescriptionListInterface {
export interface BodyResourceDescriptionInterface { export interface BodyResourceDescriptionInterface {
name: string, name: string,
img: string img: string,
level?: number
} }
export const PLAYER_RESOURCES: BodyResourceDescriptionListInterface = { export const PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {

View File

@ -23,21 +23,26 @@ export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptio
}); });
return returnArray; return returnArray;
} }
export const loadCustomTexture = (load: LoaderPlugin, texture: CharacterTexture) => {
export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise<BodyResourceDescriptionInterface> => {
const name = 'customCharacterTexture'+texture.id; const name = 'customCharacterTexture'+texture.id;
load.spritesheet(name,texture.url,{frameWidth: 32, frameHeight: 32}); const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level}
return name; return createLoadingPromise(loaderPlugin, playerResourceDescriptor);
} }
export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturePlugin: TextureManager, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => { export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => {
const promisesList:Promise<void>[] = []; const promisesList:Promise<unknown>[] = [];
texturekeys.forEach((textureKey: string|BodyResourceDescriptionInterface) => { texturekeys.forEach((textureKey: string|BodyResourceDescriptionInterface) => {
try {
//TODO refactor
const playerResourceDescriptor = getRessourceDescriptor(textureKey); const playerResourceDescriptor = getRessourceDescriptor(textureKey);
if(!texturePlugin.exists(playerResourceDescriptor.name)) { if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
console.log('Loading '+playerResourceDescriptor.name)
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor)); promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor));
} }
}) }catch (err){
console.error(err);
}
});
let returnPromise:Promise<Array<string|BodyResourceDescriptionInterface>>; let returnPromise:Promise<Array<string|BodyResourceDescriptionInterface>>;
if (promisesList.length > 0) { if (promisesList.length > 0) {
loadPlugin.start(); loadPlugin.start();
@ -66,8 +71,14 @@ export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptio
} }
const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) => { const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) => {
return new Promise<void>((res, rej) => { return new Promise<BodyResourceDescriptionInterface>((res) => {
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, {frameWidth: 32, frameHeight: 32}); if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
loadPlugin.once('filecomplete-spritesheet-'+playerResourceDescriptor.name, () => res()); return res(playerResourceDescriptor);
}
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, {
frameWidth: 32,
frameHeight: 32
});
loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor));
}); });
} }

View File

@ -1,7 +1,6 @@
import {GameScene} from "../Game/GameScene"; import {GameScene} from "../Game/GameScene";
import {PointInterface} from "../../Connexion/ConnexionModels"; import {PointInterface} from "../../Connexion/ConnexionModels";
import {Character} from "../Entity/Character"; import {Character} from "../Entity/Character";
import {Sprite} from "./Sprite";
/** /**
* Class representing the sprite of a remote player (a player that plays on another computer) * Class representing the sprite of a remote player (a player that plays on another computer)
@ -23,6 +22,11 @@ export class RemotePlayer extends Character {
//set data //set data
this.userId = userId; this.userId = userId;
//todo: implement on click action
/*this.playerName.setInteractive();
this.playerName.on('pointerup', () => {
});*/
} }
updatePosition(position: PointInterface): void { updatePosition(position: PointInterface): void {

View File

@ -14,7 +14,7 @@ export class SpeechBubble {
const bubbleWidth = bubblePadding * 2 + text.length * 10; const bubbleWidth = bubblePadding * 2 + text.length * 10;
const arrowHeight = bubbleHeight / 4; const arrowHeight = bubbleHeight / 4;
this.bubble = scene.add.graphics({ x: player.x + 16, y: player.y - 80 }); this.bubble = scene.add.graphics({ x: 16, y: -80 });
player.add(this.bubble); player.add(this.bubble);
// Bubble shadow // Bubble shadow

View File

@ -49,6 +49,10 @@ export class GameMap {
this.lastProperties = newProps; this.lastProperties = newProps;
} }
public getCurrentProperties(): Map<string, string|boolean|number> {
return this.lastProperties;
}
private getProperties(key: number): Map<string, string|boolean|number> { private getProperties(key: number): Map<string, string|boolean|number> {
const properties = new Map<string, string|boolean|number>(); const properties = new Map<string, string|boolean|number>();

View File

@ -30,10 +30,11 @@ import {RemotePlayer} from "../Entity/RemotePlayer";
import {Queue} from 'queue-typescript'; import {Queue} from 'queue-typescript';
import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
import {lazyLoadPlayerCharacterTextures} from "../Entity/PlayerTexturesLoadingManager"; import {lazyLoadPlayerCharacterTextures, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager";
import { import {
CenterListener, CenterListener,
EXIT_MESSAGE_PROPERTIES, JITSI_MESSAGE_PROPERTIES, EXIT_MESSAGE_PROPERTIES,
JITSI_MESSAGE_PROPERTIES,
layoutManager, layoutManager,
LayoutMode, LayoutMode,
ON_ACTION_TRIGGER_BUTTON, ON_ACTION_TRIGGER_BUTTON,
@ -72,6 +73,8 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha
import {TextureError} from "../../Exception/TextureError"; import {TextureError} from "../../Exception/TextureError";
import {addLoader} from "../Components/Loader"; import {addLoader} from "../Components/Loader";
import {ErrorSceneName} from "../Reconnecting/ErrorScene"; import {ErrorSceneName} from "../Reconnecting/ErrorScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface|null, initPosition: PointInterface|null,
@ -116,7 +119,7 @@ export class GameScene extends ResizableScene implements CenterListener {
MapPlayers!: Phaser.Physics.Arcade.Group; MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey : Map<number, RemotePlayer> = new Map<number, RemotePlayer>(); MapPlayersByKey : Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
Map!: Phaser.Tilemaps.Tilemap; Map!: Phaser.Tilemaps.Tilemap;
Layers!: Array<Phaser.Tilemaps.TilemapLayer>; Layers!: Array<Phaser.Tilemaps.StaticTilemapLayer>;
Objects!: Array<Phaser.Physics.Arcade.Sprite>; Objects!: Array<Phaser.Physics.Arcade.Sprite>;
mapFile!: ITiledMap; mapFile!: ITiledMap;
groups: Map<number, Sprite>; groups: Map<number, Sprite>;
@ -157,7 +160,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private actionableItems: Map<number, ActionableItem> = new Map<number, ActionableItem>(); private actionableItems: Map<number, ActionableItem> = new Map<number, ActionableItem>();
// The item that can be selected by pressing the space key. // The item that can be selected by pressing the space key.
private outlinedItem: ActionableItem|null = null; private outlinedItem: ActionableItem|null = null;
private userInputManager!: UserInputManager; public userInputManager!: UserInputManager;
private isReconnecting: boolean = false; private isReconnecting: boolean = false;
private startLayerName!: string | null; private startLayerName!: string | null;
private openChatIcon!: OpenChatIcon; private openChatIcon!: OpenChatIcon;
@ -186,7 +189,13 @@ export class GameScene extends ResizableScene implements CenterListener {
//hook preload scene //hook preload scene
preload(): void { preload(): void {
addLoader(this); const localUser = localUserStore.getLocalUser();
const textures = localUser?.textures;
if (textures) {
for (const texture of textures) {
loadCustomTexture(this.load, texture);
}
}
this.load.image(openChatIconName, 'resources/objects/talk.png'); this.load.image(openChatIconName, 'resources/objects/talk.png');
this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => {
@ -210,6 +219,8 @@ export class GameScene extends ResizableScene implements CenterListener {
this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32}); this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32});
this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
addLoader(this);
} }
// FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving.
@ -344,11 +355,11 @@ export class GameScene extends ResizableScene implements CenterListener {
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
//add layer on map //add layer on map
this.Layers = new Array<Phaser.Tilemaps.TilemapLayer>(); this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>();
let depth = -2; let depth = -2;
for (const layer of this.mapFile.layers) { for (const layer of this.mapFile.layers) {
if (layer.type === 'tilelayer') { if (layer.type === 'tilelayer') {
this.addLayer(this.Map.createLayer(layer.name, this.Terrains, 0, 0).setDepth(depth)); this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth));
const exitSceneUrl = this.getExitSceneUrl(layer); const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl !== undefined) { if (exitSceneUrl !== undefined) {
@ -529,6 +540,7 @@ export class GameScene extends ResizableScene implements CenterListener {
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
this.GlobalMessageManager = new GlobalMessageManager(this.connection); this.GlobalMessageManager = new GlobalMessageManager(this.connection);
this.UserMessageManager = new UserMessageManager(this.connection); this.UserMessageManager = new UserMessageManager(this.connection);
this.UserMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
const self = this; const self = this;
this.simplePeer.registerPeerConnectionListener({ this.simplePeer.registerPeerConnectionListener({
@ -625,6 +637,15 @@ export class GameScene extends ResizableScene implements CenterListener {
} }
} }
private safeParseJSONstring(jsonString: string|undefined, propertyName: string) {
try {
return jsonString ? JSON.parse(jsonString) : {};
} catch(e) {
console.warn('Invalid JSON found in property "' + propertyName + '" of the map:' + jsonString, e);
return {}
}
}
private triggerOnMapLayerPropertyChange(){ private triggerOnMapLayerPropertyChange(){
/* @deprecated /* @deprecated
this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => { this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => {
@ -656,7 +677,7 @@ export class GameScene extends ResizableScene implements CenterListener {
coWebsiteManager.closeCoWebsite(); coWebsiteManager.closeCoWebsite();
}else{ }else{
const openWebsiteFunction = () => { const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(newValue as string); coWebsiteManager.loadCoWebsite(newValue as string, allProps.get('openWebsitePolicy') as string | undefined);
layoutManager.removeActionButton('openWebsite', this.userInputManager); layoutManager.removeActionButton('openWebsite', this.userInputManager);
}; };
@ -680,11 +701,13 @@ export class GameScene extends ResizableScene implements CenterListener {
this.stopJitsi(); this.stopJitsi();
}else{ }else{
const openJitsiRoomFunction = () => { const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance);
if (JITSI_PRIVATE_MODE) { if (JITSI_PRIVATE_MODE) {
const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined;
this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag);
this.connection.emitQueryJitsiJwtMessage(roomName, adminTag);
} else { } else {
this.startJitsi(newValue as string); this.startJitsi(roomName, undefined);
} }
layoutManager.removeActionButton('jitsiRoom', this.userInputManager); layoutManager.removeActionButton('jitsiRoom', this.userInputManager);
} }
@ -725,6 +748,10 @@ export class GameScene extends ResizableScene implements CenterListener {
if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey);
urlManager.pushStartLayerNameToUrl(hash); urlManager.pushStartLayerNameToUrl(hash);
if (roomId !== this.scene.key) { if (roomId !== this.scene.key) {
if (this.scene.get(roomId) === null) {
console.error("next room not loaded", exitKey);
return;
}
this.cleanupClosingScene(); this.cleanupClosingScene();
this.scene.stop(); this.scene.stop();
this.scene.remove(this.scene.key); this.scene.remove(this.scene.key);
@ -878,13 +905,13 @@ export class GameScene extends ResizableScene implements CenterListener {
this.cameras.main.setZoom(ZOOM_LEVEL); this.cameras.main.setZoom(ZOOM_LEVEL);
} }
addLayer(Layer : Phaser.Tilemaps.TilemapLayer){ addLayer(Layer : Phaser.Tilemaps.StaticTilemapLayer){
this.Layers.push(Layer); this.Layers.push(Layer);
} }
createCollisionWithPlayer() { createCollisionWithPlayer() {
//add collision layer //add collision layer
this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => { this.Layers.forEach((Layer: Phaser.Tilemaps.StaticTilemapLayer) => {
this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => { this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => {
//this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name)
}); });
@ -902,7 +929,7 @@ export class GameScene extends ResizableScene implements CenterListener {
createCurrentPlayer(){ createCurrentPlayer(){
//TODO create animation moving between exit and start //TODO create animation moving between exit and start
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.textures, this.characterLayers); const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.characterLayers);
try { try {
this.CurrentPlayer = new Player( this.CurrentPlayer = new Player(
this, this,
@ -1096,7 +1123,7 @@ export class GameScene extends ResizableScene implements CenterListener {
return; return;
} }
const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, this.textures, addPlayerData.characterLayers); const texturesPromise = lazyLoadPlayerCharacterTextures(this.load, addPlayerData.characterLayers);
const player = new RemotePlayer( const player = new RemotePlayer(
addPlayerData.userId, addPlayerData.userId,
this, this,
@ -1221,6 +1248,7 @@ export class GameScene extends ResizableScene implements CenterListener {
private reposition(): void { private reposition(): void {
this.presentationModeSprite.setY(this.game.renderer.height - 2); this.presentationModeSprite.setY(this.game.renderer.height - 2);
this.chatModeSprite.setY(this.game.renderer.height - 2); this.chatModeSprite.setY(this.game.renderer.height - 2);
this.openChatIcon.setY(this.game.renderer.height - 2);
// Recompute camera offset if needed // Recompute camera offset if needed
this.updateCameraOffset(); this.updateCameraOffset();
@ -1247,7 +1275,11 @@ export class GameScene extends ResizableScene implements CenterListener {
} }
public startJitsi(roomName: string, jwt?: string): void { public startJitsi(roomName: string, jwt?: string): void {
jitsiFactory.start(roomName, this.playerName, jwt); const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string|undefined, 'jitsiConfig');
const jitsiInterfaceConfig = this.safeParseJSONstring(allProps.get("jitsiInterfaceConfig") as string|undefined, 'jitsiInterfaceConfig');
jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig);
this.connection.setSilent(true); this.connection.setSilent(true);
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();
@ -1265,5 +1297,14 @@ export class GameScene extends ResizableScene implements CenterListener {
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi'); mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
} }
private bannedUser(){
this.cleanupClosingScene();
this.userInputManager.clearAllKeys();
this.scene.start(ErrorSceneName, {
title: 'Banned',
subTitle: 'You was banned of WorkAdventure',
message: 'If you want more information, you can contact us: workadventure@thecodingmachine.com'
});
}
} }

View File

@ -43,8 +43,9 @@ export class ActionableItem {
} }
this.isSelectable = true; this.isSelectable = true;
if (this.sprite.pipeline) { if (this.sprite.pipeline) {
this.sprite.setPipeline(OutlinePipeline.KEY); // Commented out to try to fix MacOS issue
this.sprite.pipeline.set2f('uTextureSize', this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height); /*this.sprite.setPipeline(OutlinePipeline.KEY);
this.sprite.pipeline.set2f('uTextureSize', this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height);*/
} }
} }
@ -56,7 +57,8 @@ export class ActionableItem {
return; return;
} }
this.isSelectable = false; this.isSelectable = false;
this.sprite.resetPipeline(); // Commented out to try to fix MacOS issue
//this.sprite.resetPipeline();
} }
/** /**

View File

@ -0,0 +1,41 @@
import {ResizableScene} from "./ResizableScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager";
import {CharacterTexture} from "../../Connexion/LocalUser";
export abstract class AbstractCharacterScene extends ResizableScene {
loadCustomSceneSelectCharacters() : Promise<BodyResourceDescriptionInterface[]> {
const textures = this.getTextures();
const promises : Promise<BodyResourceDescriptionInterface>[] = [];
if (textures) {
for (const texture of textures) {
if (texture.level === -1) {
continue;
}
promises.push(loadCustomTexture(this.load, texture));
}
}
return Promise.all(promises)
}
loadSelectSceneCharacters() : Promise<BodyResourceDescriptionInterface[]> {
const textures = this.getTextures();
const promises: Promise<BodyResourceDescriptionInterface>[] = [];
if (textures) {
for (const texture of textures) {
if (texture.level !== -1) {
continue;
}
promises.push(loadCustomTexture(this.load, texture));
}
}
return Promise.all(promises)
}
private getTextures() : CharacterTexture[]|undefined{
const localUser = localUserStore.getLocalUser();
return localUser?.textures;
}
}

View File

@ -10,6 +10,7 @@ import {ResizableScene} from "./ResizableScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import {localUserStore} from "../../Connexion/LocalUserStore";
import {addLoader} from "../Components/Loader"; import {addLoader} from "../Components/Loader";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene";
export const CustomizeSceneName = "CustomizeScene"; export const CustomizeSceneName = "CustomizeScene";
@ -20,7 +21,7 @@ enum CustomizeTextures{
arrowUp = "arrow_up", arrowUp = "arrow_up",
} }
export class CustomizeScene extends ResizableScene { export class CustomizeScene extends AbstractCharacterScene {
private textField!: TextField; private textField!: TextField;
private enterField!: TextField; private enterField!: TextField;
@ -48,29 +49,21 @@ export class CustomizeScene extends ResizableScene {
preload() { preload() {
addLoader(this); addLoader(this);
this.layers = loadAllLayers(this.load);
this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
if(!bodyResourceDescription.level){
throw 'Texture level is null';
}
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
});
});
this.load.image(CustomizeTextures.arrowRight, "resources/objects/arrow_right.png"); this.load.image(CustomizeTextures.arrowRight, "resources/objects/arrow_right.png");
this.load.image(CustomizeTextures.icon, "resources/logos/tcm_full.png"); this.load.image(CustomizeTextures.icon, "resources/logos/tcm_full.png");
this.load.image(CustomizeTextures.arrowUp, "resources/objects/arrow_up.png"); this.load.image(CustomizeTextures.arrowUp, "resources/objects/arrow_up.png");
this.load.bitmapFont(CustomizeTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); this.load.bitmapFont(CustomizeTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.layers = loadAllLayers(this.load);
const localUser = localUserStore.getLocalUser();
const textures = localUser?.textures;
if (textures) {
for (const texture of textures) {
if(texture.level === -1){
continue;
}
loadCustomTexture(this.load, texture);
const name = 'customCharacterTexture'+texture.id;
this.layers[texture.level].unshift({
name,
img: texture.url
});
}
}
} }
create() { create() {

View File

@ -2,7 +2,6 @@ import {gameManager} from "../Game/GameManager";
import {TextField} from "../Components/TextField"; import {TextField} from "../Components/TextField";
import {TextInput} from "../Components/TextInput"; import {TextInput} from "../Components/TextInput";
import Image = Phaser.GameObjects.Image; import Image = Phaser.GameObjects.Image;
import {cypressAsserter} from "../../Cypress/CypressAsserter";
import {SelectCharacterSceneName} from "./SelectCharacterScene"; import {SelectCharacterSceneName} from "./SelectCharacterScene";
import {ResizableScene} from "./ResizableScene"; import {ResizableScene} from "./ResizableScene";
@ -29,16 +28,13 @@ export class LoginScene extends ResizableScene {
} }
preload() { preload() {
cypressAsserter.preloadStarted();
//this.load.image(LoginTextures.playButton, "resources/objects/play_button.png"); //this.load.image(LoginTextures.playButton, "resources/objects/play_button.png");
this.load.image(LoginTextures.icon, "resources/logos/tcm_full.png"); this.load.image(LoginTextures.icon, "resources/logos/tcm_full.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
cypressAsserter.preloadFinished();
} }
create() { create() {
cypressAsserter.initStarted();
this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Enter your name:'); this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Enter your name:');
this.nameInput = new TextInput(this, this.game.renderer.width / 2, 70, 8, this.name,(text: string) => { this.nameInput = new TextInput(this, this.game.renderer.width / 2, 70, 8, this.name,(text: string) => {
@ -59,8 +55,6 @@ export class LoginScene extends ResizableScene {
} }
this.login(this.name); this.login(this.name);
}); });
cypressAsserter.initFinished();
} }
update(time: number, delta: number): void { update(time: number, delta: number): void {

View File

@ -9,6 +9,7 @@ import {localUserStore} from "../../Connexion/LocalUserStore";
import {loadAllDefaultModels, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; import {loadAllDefaultModels, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager";
import {addLoader} from "../Components/Loader"; import {addLoader} from "../Components/Loader";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene";
//todo: put this constants in a dedicated file //todo: put this constants in a dedicated file
@ -21,7 +22,7 @@ enum LoginTextures {
customizeButtonSelected = "customize_button_selected" customizeButtonSelected = "customize_button_selected"
} }
export class SelectCharacterScene extends ResizableScene { export class SelectCharacterScene extends AbstractCharacterScene {
private readonly nbCharactersPerRow = 6; private readonly nbCharactersPerRow = 6;
private textField!: TextField; private textField!: TextField;
private pressReturnField!: TextField; private pressReturnField!: TextField;
@ -44,6 +45,13 @@ export class SelectCharacterScene extends ResizableScene {
preload() { preload() {
addLoader(this); addLoader(this);
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription);
});
})
this.load.image(LoginTextures.playButton, "resources/objects/play_button.png"); this.load.image(LoginTextures.playButton, "resources/objects/play_button.png");
this.load.image(LoginTextures.icon, "resources/logos/tcm_full.png"); this.load.image(LoginTextures.icon, "resources/logos/tcm_full.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
@ -52,17 +60,7 @@ export class SelectCharacterScene extends ResizableScene {
this.load.image(LoginTextures.customizeButton, 'resources/objects/customize.png'); this.load.image(LoginTextures.customizeButton, 'resources/objects/customize.png');
this.load.image(LoginTextures.customizeButtonSelected, 'resources/objects/customize_selected.png'); this.load.image(LoginTextures.customizeButtonSelected, 'resources/objects/customize_selected.png');
const localUser = localUserStore.getLocalUser(); addLoader(this);
const textures = localUser?.textures;
if (textures) {
for (const texture of textures) {
if(texture.level !== -1){
continue;
}
const name = loadCustomTexture(this.load, texture);
this.playerModels.push({name: name, img: texture.url});
}
}
} }
create() { create() {

View File

@ -3,7 +3,9 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha
import {gameManager} from "../Game/GameManager"; import {gameManager} from "../Game/GameManager";
import {localUserStore} from "../../Connexion/LocalUserStore"; import {localUserStore} from "../../Connexion/LocalUserStore";
import {mediaManager} from "../../WebRtc/MediaManager"; import {mediaManager} from "../../WebRtc/MediaManager";
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager";
export const MenuSceneName = 'MenuScene'; export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu'; const gameMenuKey = 'gameMenu';
@ -21,6 +23,7 @@ export class MenuScene extends Phaser.Scene {
private menuElement!: Phaser.GameObjects.DOMElement; private menuElement!: Phaser.GameObjects.DOMElement;
private gameQualityMenuElement!: Phaser.GameObjects.DOMElement; private gameQualityMenuElement!: Phaser.GameObjects.DOMElement;
private gameShareElement!: Phaser.GameObjects.DOMElement; private gameShareElement!: Phaser.GameObjects.DOMElement;
private gameReportElement!: ReportMenu;
private sideMenuOpened = false; private sideMenuOpened = false;
private settingsMenuOpened = false; private settingsMenuOpened = false;
private gameShareOpened = false; private gameShareOpened = false;
@ -40,20 +43,21 @@ export class MenuScene extends Phaser.Scene {
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html');
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html');
this.load.html(gameShare, 'resources/html/gameShare.html'); this.load.html(gameShare, 'resources/html/gameShare.html');
this.load.html(gameReportKey, gameReportRessource);
} }
create() { create() {
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey); this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
this.menuElement.setOrigin(0); this.menuElement.setOrigin(0);
this.revealMenusAfterInit(this.menuElement, 'gameMenu'); MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu');
const middleX = (window.innerWidth / 3) - 298; const middleX = (window.innerWidth / 3) - 298;
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey); this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
this.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality'); MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, 'gameQuality');
this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare); this.gameShareElement = this.add.dom(middleX, -400).createFromCache(gameShare);
this.revealMenusAfterInit(this.gameShareElement, gameShare); MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
this.gameShareElement.addListener('click'); this.gameShareElement.addListener('click');
this.gameShareElement.on('click', (event:MouseEvent) => { this.gameShareElement.on('click', (event:MouseEvent) => {
event.preventDefault(); event.preventDefault();
@ -64,6 +68,12 @@ export class MenuScene extends Phaser.Scene {
} }
}); });
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
mediaManager.setShowReportModalCallBacks((userId, userName) => {
this.closeAll();
this.gameReportElement.open(parseInt(userId), userName);
});
this.input.keyboard.on('keyup-TAB', () => { this.input.keyboard.on('keyup-TAB', () => {
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
}); });
@ -77,7 +87,8 @@ export class MenuScene extends Phaser.Scene {
this.menuElement.on('click', this.onMenuClick.bind(this)); this.menuElement.on('click', this.onMenuClick.bind(this));
} }
private revealMenusAfterInit(menuElement: Phaser.GameObjects.DOMElement, rootDomId: string) { //todo put this method in a parent menuElement class
static revealMenusAfterInit(menuElement: Phaser.GameObjects.DOMElement, rootDomId: string) {
//Dom elements will appear inside the viewer screen when creating before being moved out of it, which create a flicker effect. //Dom elements will appear inside the viewer screen when creating before being moved out of it, which create a flicker effect.
//To prevent this, we put a 'hidden' attribute on the root element, we remove it only after the init is done. //To prevent this, we put a 'hidden' attribute on the root element, we remove it only after the init is done.
setTimeout(() => { setTimeout(() => {
@ -98,6 +109,11 @@ export class MenuScene extends Phaser.Scene {
const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement; const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement;
adminSection.hidden = false; adminSection.hidden = false;
} }
//TODO bind with future metadata of card
//if (connectionManager.getConnexionType === GameConnexionTypes.anonymous){
const adminSection = this.menuElement.getChildByID('socialLinks') as HTMLElement;
adminSection.hidden = false;
//}
this.tweens.add({ this.tweens.add({
targets: this.menuElement, targets: this.menuElement,
x: openedSideMenuX, x: openedSideMenuX,
@ -222,6 +238,9 @@ export class MenuScene extends Phaser.Scene {
} }
private onMenuClick(event:MouseEvent) { private onMenuClick(event:MouseEvent) {
if((event?.target as HTMLInputElement).classList.contains('not-button')){
return;
}
event.preventDefault(); event.preventDefault();
switch ((event?.target as HTMLInputElement).id) { switch ((event?.target as HTMLInputElement).id) {
@ -280,5 +299,6 @@ export class MenuScene extends Phaser.Scene {
private closeAll(){ private closeAll(){
this.closeGameQualityMenu(); this.closeGameQualityMenu();
this.closeGameShare(); this.closeGameShare();
this.gameReportElement.close();
} }
} }

View File

@ -0,0 +1,119 @@
import {MenuScene} from "./MenuScene";
import {gameManager} from "../Game/GameManager";
import {blackListManager} from "../../WebRtc/BlackListManager";
export const gameReportKey = 'gameReport';
export const gameReportRessource = 'resources/html/gameReport.html';
export class ReportMenu extends Phaser.GameObjects.DOMElement {
private opened: boolean = false;
private userId!: number;
private userName!: string|undefined;
private anonymous: boolean;
constructor(scene: Phaser.Scene, anonymous: boolean) {
super(scene, -2000, -2000);
this.anonymous = anonymous;
this.createFromCache(gameReportKey);
if (this.anonymous) {
const divToHide = this.getChildByID('reportSection') as HTMLElement;
divToHide.hidden = true;
const textToHide = this.getChildByID('askActionP') as HTMLElement;
textToHide.hidden = true;
}
scene.add.existing(this);
MenuScene.revealMenusAfterInit(this, gameReportKey);
this.addListener('click');
this.on('click', (event:MouseEvent) => {
event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameReportFormSubmit') {
this.submitReport();
} else if((event?.target as HTMLInputElement).id === 'gameReportFormCancel') {
this.close();
} else if((event?.target as HTMLInputElement).id === 'toggleBlockButton') {
this.toggleBlock();
}
});
}
public open(userId: number, userName: string|undefined): void {
if (this.opened) {
this.close();
return;
}
this.userId = userId;
this.userName = userName;
const mainEl = this.getChildByID('gameReport') as HTMLElement;
this.x = this.getCenteredX(mainEl);
this.y = this.getHiddenY(mainEl);
const gameTitleReport = this.getChildByID('nameReported') as HTMLElement;
gameTitleReport.innerText = userName || '';
const blockButton = this.getChildByID('toggleBlockButton') as HTMLElement;
blockButton.innerText = blackListManager.isBlackListed(this.userId) ? 'Unblock this user' : 'Block this user';
this.opened = true;
gameManager.getCurrentGameScene(this.scene).userInputManager.clearAllKeys();
this.scene.tweens.add({
targets: this,
y: this.getCenteredY(mainEl),
duration: 1000,
ease: 'Power3'
});
}
public close(): void {
this.opened = false;
gameManager.getCurrentGameScene(this.scene).userInputManager.initKeyBoardEvent();
const mainEl = this.getChildByID('gameReport') as HTMLElement;
this.scene.tweens.add({
targets: this,
y: this.getHiddenY(mainEl),
duration: 1000,
ease: 'Power3'
});
}
//todo: into a parent class?
private getCenteredX(mainEl: HTMLElement): number {
return window.innerWidth / 4 - mainEl.clientWidth / 2;
}
private getHiddenY(mainEl: HTMLElement): number {
return - mainEl.clientHeight - 50;
}
private getCenteredY(mainEl: HTMLElement): number {
return window.innerHeight / 4 - mainEl.clientHeight / 2;
}
private toggleBlock(): void {
!blackListManager.isBlackListed(this.userId) ? blackListManager.blackList(this.userId) : blackListManager.cancelBlackList(this.userId);
this.close();
}
private submitReport(): void{
const gamePError = this.getChildByID('gameReportErr') as HTMLParagraphElement;
gamePError.innerText = '';
gamePError.style.display = 'none';
const gameTextArea = this.getChildByID('gameReportInput') as HTMLInputElement;
const gameIdUserReported = this.getChildByID('idUserReported') as HTMLInputElement;
if(!gameTextArea || !gameTextArea.value || !gameIdUserReported || !gameIdUserReported.value){
gamePError.innerText = 'Report message cannot to be empty.';
gamePError.style.display = 'block';
return;
}
gameManager.getCurrentGameScene(this.scene).connection.emitReportPlayerMessage(
parseInt(gameIdUserReported.value),
gameTextArea.value
);
this.close();
}
}

View File

@ -1,4 +1,4 @@
export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.MultiPipeline { export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline {
// the unique id of this pipeline // the unique id of this pipeline
public static readonly KEY = 'Outline'; public static readonly KEY = 'Outline';

View File

@ -59,7 +59,11 @@ export class UserInputManager {
]; ];
} }
clearAllInputKeyboard(){ clearAllListeners(){
this.Scene.input.keyboard.removeAllListeners();
}
clearAllKeys(){
this.Scene.input.keyboard.removeAllKeys(); this.Scene.input.keyboard.removeAllKeys();
} }

View File

@ -0,0 +1,24 @@
import {Subject} from 'rxjs';
class BlackListManager {
private list: number[] = [];
public onBlockStream: Subject<number> = new Subject();
public onUnBlockStream: Subject<number> = new Subject();
isBlackListed(userId: number): boolean {
return this.list.find((data) => data === userId) !== undefined;
}
blackList(userId: number): void {
if (this.isBlackListed(userId)) return;
this.list.push(userId);
this.onBlockStream.next(userId);
}
cancelBlackList(userId: number): void {
this.list.splice(this.list.findIndex(data => data === userId), 1);
this.onUnBlockStream.next(userId);
}
}
export const blackListManager = new BlackListManager();

View File

@ -42,7 +42,7 @@ class CoWebsiteManager {
this.opened = iframeStates.opened; this.opened = iframeStates.opened;
} }
public loadCoWebsite(url: string): void { public loadCoWebsite(url: string, allowPolicy?: string): void {
this.load(); this.load();
this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close"> this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg"> <img src="resources/logos/close.svg">
@ -56,6 +56,9 @@ class CoWebsiteManager {
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.id = 'cowebsite-iframe'; iframe.id = 'cowebsite-iframe';
iframe.src = url; iframe.src = url;
if (allowPolicy) {
iframe.allow = allowPolicy;
}
const onloadPromise = new Promise((resolve) => { const onloadPromise = new Promise((resolve) => {
iframe.onload = () => resolve(); iframe.onload = () => resolve();
}); });

View File

@ -1,5 +1,5 @@
import {HtmlUtils} from "./HtmlUtils"; import {HtmlUtils} from "./HtmlUtils";
import {mediaManager, ReportCallback} from "./MediaManager"; import {mediaManager, ReportCallback, ShowReportCallBack} from "./MediaManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager"; import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager"; import {GameConnexionTypes} from "../Url/UrlManager";
@ -61,7 +61,7 @@ export class DiscussionManager {
const inputMessage: HTMLInputElement = document.createElement('input'); const inputMessage: HTMLInputElement = document.createElement('input');
inputMessage.onfocus = () => { inputMessage.onfocus = () => {
if(this.userInputManager) { if(this.userInputManager) {
this.userInputManager.clearAllInputKeyboard(); this.userInputManager.clearAllKeys();
} }
} }
inputMessage.onblur = () => { inputMessage.onblur = () => {
@ -99,7 +99,7 @@ export class DiscussionManager {
name: string|undefined, name: string|undefined,
img?: string|undefined, img?: string|undefined,
isMe: boolean = false, isMe: boolean = false,
reportCallback?: ReportCallback showReportCallBack?: ShowReportCallBack
) { ) {
const divParticipant: HTMLDivElement = document.createElement('div'); const divParticipant: HTMLDivElement = document.createElement('div');
divParticipant.classList.add('participant'); divParticipant.classList.add('participant');
@ -128,8 +128,8 @@ export class DiscussionManager {
reportBanUserAction.classList.add('report-btn') reportBanUserAction.classList.add('report-btn')
reportBanUserAction.innerText = 'Report'; reportBanUserAction.innerText = 'Report';
reportBanUserAction.addEventListener('click', () => { reportBanUserAction.addEventListener('click', () => {
if(reportCallback) { if(showReportCallBack) {
mediaManager.showReportModal(`${userId}`, name ?? '', reportCallback); showReportCallBack(`${userId}`, name);
}else{ }else{
console.info('report feature is not activated!'); console.info('report feature is not activated!');
} }

View File

@ -1,27 +1,45 @@
export class HtmlUtils { export class HtmlUtils {
public static getElementByIdOrFail<T extends HTMLElement>(id: string): T { public static getElementByIdOrFail<T extends HTMLElement>(id: string): T {
const elem = document.getElementById(id); const elem = document.getElementById(id);
if (elem === null) { if (HtmlUtils.isHtmlElement<T>(elem)) {
return elem;
}
throw new Error("Cannot find HTML element with id '"+id+"'"); throw new Error("Cannot find HTML element with id '"+id+"'");
} }
// FIXME: does not check the type of the returned type
return elem as T; public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
const elem = document.querySelector<T>(selector);
if (HtmlUtils.isHtmlElement<T>(elem)) {
return elem;
}
throw new Error("Cannot find HTML element with selector '"+selector+"'");
} }
public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T { public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T {
const elem = document.getElementById(id); const elem = document.getElementById(id);
if (elem === null) { if (HtmlUtils.isHtmlElement<T>(elem)) {
elem.remove();
return elem;
}
throw new Error("Cannot find HTML element with id '"+id+"'"); throw new Error("Cannot find HTML element with id '"+id+"'");
} }
// FIXME: does not check the type of the returned type
elem.remove(); private static escapeHtml(html: string): string {
return elem as T; const text = document.createTextNode(html);
const p = document.createElement('p');
p.appendChild(text);
return p.innerHTML;
} }
public static urlify(text: string): string { public static urlify(text: string): string {
const urlRegex = /(https?:\/\/[^\s]+)/g; const urlRegex = /(https?:\/\/[^\s]+)/g;
text = HtmlUtils.escapeHtml(text);
return text.replace(urlRegex, (url: string) => { return text.replace(urlRegex, (url: string) => {
return '<a href="' + url + '" target="_blank" style=":visited {color: white}">' + url + '</a>'; return '<a href="' + url + '" target="_blank" style=":visited {color: white}">' + url + '</a>';
}) });
}
private static isHtmlElement<T extends HTMLElement>(elem: HTMLElement | null): elem is T {
return elem !== null;
} }
} }

View File

@ -3,11 +3,21 @@ import {mediaManager} from "./MediaManager";
import {coWebsiteManager} from "./CoWebsiteManager"; import {coWebsiteManager} from "./CoWebsiteManager";
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
const interfaceConfig = { const getDefaultConfig = () => {
return {
startWithAudioMuted: !mediaManager.constraintsMedia.audio,
startWithVideoMuted: mediaManager.constraintsMedia.video === false,
prejoinPageEnabled: false
}
}
const defaultInterfaceConfig = {
SHOW_CHROME_EXTENSION_BANNER: false, SHOW_CHROME_EXTENSION_BANNER: false,
MOBILE_APP_PROMO: false, MOBILE_APP_PROMO: false,
HIDE_INVITE_MORE_HEADER: true, HIDE_INVITE_MORE_HEADER: true,
DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
DISABLE_VIDEO_BACKGROUND: true,
// Note: hiding brand does not seem to work, we probably need to put this on the server side. // Note: hiding brand does not seem to work, we probably need to put this on the server side.
SHOW_BRAND_WATERMARK: false, SHOW_BRAND_WATERMARK: false,
@ -25,13 +35,40 @@ const interfaceConfig = {
], ],
}; };
const slugify = (...args: (string | number)[]): string => {
const value = args.join(' ')
return value
.normalize('NFD') // split an accented letter in the base letter and the accent
.replace(/[\u0300-\u036f]/g, '') // remove all previously split accents
.toLowerCase()
.trim()
.replace(/[^a-z0-9 ]/g, '') // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, '-') // separator
}
class JitsiFactory { class JitsiFactory {
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
private audioCallback = this.onAudioChange.bind(this); private audioCallback = this.onAudioChange.bind(this);
private videoCallback = this.onVideoChange.bind(this); private videoCallback = this.onVideoChange.bind(this);
public start(roomName: string, playerName:string, jwt?: string): void { /**
* Slugifies the room name and prepends the room name with the instance
*/
public getRoomName(roomName: string, instance: string): string {
return slugify(instance.replace('/', '-') + "-" + roomName);
}
public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object): void {
coWebsiteManager.insertCoWebsite((cowebsiteDiv => { coWebsiteManager.insertCoWebsite((cowebsiteDiv => {
// Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
// after some time the URLs get so huge that loading the iframe
// becomes slow and eventually breaks completely. Thus lets just
// clear jitsi local storage before starting a new conference.
window.localStorage.removeItem("jitsiLocalStorage");
const domain = JITSI_URL; const domain = JITSI_URL;
const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any
roomName: roomName, roomName: roomName,
@ -39,12 +76,8 @@ class JitsiFactory {
width: "100%", width: "100%",
height: "100%", height: "100%",
parentNode: cowebsiteDiv, parentNode: cowebsiteDiv,
configOverwrite: { configOverwrite: {...config, ...getDefaultConfig()},
startWithAudioMuted: !mediaManager.constraintsMedia.audio, interfaceConfigOverwrite: {...defaultInterfaceConfig, ...interfaceConfig}
startWithVideoMuted: mediaManager.constraintsMedia.video === false,
prejoinPageEnabled: false
},
interfaceConfigOverwrite: interfaceConfig,
}; };
if (!options.jwt) { if (!options.jwt) {
delete options.jwt; delete options.jwt;
@ -87,7 +120,6 @@ class JitsiFactory {
mediaManager.enableCamera(); mediaManager.enableCamera();
} }
} }
} }
export const jitsiFactory = new JitsiFactory(); export const jitsiFactory = new JitsiFactory();

View File

@ -218,7 +218,7 @@ class LayoutManager {
* Tries to find the biggest available box of remaining space (this is a space where we can center the character) * Tries to find the biggest available box of remaining space (this is a space where we can center the character)
*/ */
public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} { public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} {
const game = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game'); const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas');
if (this.mode === LayoutMode.VideoChat) { if (this.mode === LayoutMode.VideoChat) {
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div'); const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
const htmlChildren = Array.from(children.values()); const htmlChildren = Array.from(children.values());

View File

@ -3,6 +3,7 @@ import {HtmlUtils} from "./HtmlUtils";
import {discussionManager, SendMessageCallback} from "./DiscussionManager"; import {discussionManager, SendMessageCallback} from "./DiscussionManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {VIDEO_QUALITY_SELECT} from "../Administration/ConsoleGlobalMessageManager"; import {VIDEO_QUALITY_SELECT} from "../Administration/ConsoleGlobalMessageManager";
import {UserSimplePeerInterface} from "./SimplePeer";
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
const localValueVideo = localStorage.getItem(VIDEO_QUALITY_SELECT); const localValueVideo = localStorage.getItem(VIDEO_QUALITY_SELECT);
@ -23,9 +24,9 @@ export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void; export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void;
export type ReportCallback = (message: string) => void; export type ReportCallback = (message: string) => void;
export type ShowReportCallBack = (userId: string, userName: string|undefined) => void;
// TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only) // TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only)
// TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!)
export class MediaManager { export class MediaManager {
localStream: MediaStream|null = null; localStream: MediaStream|null = null;
localScreenCapture: MediaStream|null = null; localScreenCapture: MediaStream|null = null;
@ -46,6 +47,7 @@ export class MediaManager {
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>(); updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>(); stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
showReportModalCallBacks : Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
private microphoneBtn: HTMLDivElement; private microphoneBtn: HTMLDivElement;
private cinemaBtn: HTMLDivElement; private cinemaBtn: HTMLDivElement;
private monitorBtn: HTMLDivElement; private monitorBtn: HTMLDivElement;
@ -469,8 +471,9 @@ export class MediaManager {
return this.getCamera(); return this.getCamera();
} }
addActiveVideo(userId: string, reportCallBack: ReportCallback|undefined, userName: string = ""){ addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
this.webrtcInAudio.play(); this.webrtcInAudio.play();
const userId = ''+user.userId
userName = userName.toUpperCase(); userName = userName.toUpperCase();
const color = this.getColorByString(userName); const color = this.getColorByString(userName);
@ -480,33 +483,39 @@ export class MediaManager {
<div class="connecting-spinner"></div> <div class="connecting-spinner"></div>
<div class="rtc-error" style="display: none"></div> <div class="rtc-error" style="display: none"></div>
<i id="name-${userId}" style="background-color: ${color};">${userName}</i> <i id="name-${userId}" style="background-color: ${color};">${userName}</i>
<img id="microphone-${userId}" src="resources/logos/microphone-close.svg"> <img id="microphone-${userId}" title="mute" src="resources/logos/microphone-close.svg">
` + <button id="report-${userId}" class="report">
((reportCallBack!==undefined)?`<img id="report-${userId}" class="report active" src="resources/logos/report.svg">`:'') <img title="report this user" src="resources/logos/report.svg">
+ <span>Report/Block</span>
`<video id="${userId}" autoplay></video> </button>
<video id="${userId}" autoplay></video>
<img src="resources/logos/blockSign.svg" id="blocking-${userId}" class="block-logo">
</div> </div>
`; `;
layoutManager.add(DivImportance.Normal, userId, html); layoutManager.add(DivImportance.Normal, userId, html);
if (reportCallBack) {
const reportBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(`report-${userId}`);
reportBtn.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.showReportModal(userId, userName, reportCallBack);
});
}
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId)); this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
//permit to create participant in discussion part //permit to create participant in discussion part
this.addNewParticipant(userId, userName, undefined, reportCallBack); const showReportUser = () => {
for(const callBack of this.showReportModalCallBacks){
callBack(userId, userName);
}
};
this.addNewParticipant(userId, userName, undefined, showReportUser);
const reportBanUserActionEl: HTMLImageElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>(`report-${userId}`);
reportBanUserActionEl.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showReportUser();
});
} }
addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){
userId = `screen-sharing-${userId}`; userId = this.getScreenSharingId(userId);
const html = ` const html = `
<div id="div-${userId}" class="video-container"> <div id="div-${userId}" class="video-container">
<video id="${userId}" autoplay></video> <video id="${userId}" autoplay></video>
@ -518,6 +527,10 @@ export class MediaManager {
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId)); this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
} }
private getScreenSharingId(userId: string): string {
return `screen-sharing-${userId}`;
}
disabledMicrophoneByUserId(userId: number){ disabledMicrophoneByUserId(userId: number){
const element = document.getElementById(`microphone-${userId}`); const element = document.getElementById(`microphone-${userId}`);
if(!element){ if(!element){
@ -556,6 +569,10 @@ export class MediaManager {
} }
} }
toggleBlockLogo(userId: number, show: boolean): void {
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-'+userId);
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
}
addStreamRemoteVideo(userId: string, stream : MediaStream): void { addStreamRemoteVideo(userId: string, stream : MediaStream): void {
const remoteVideo = this.remoteVideo.get(userId); const remoteVideo = this.remoteVideo.get(userId);
if (remoteVideo === undefined) { if (remoteVideo === undefined) {
@ -565,12 +582,12 @@ export class MediaManager {
} }
addStreamRemoteScreenSharing(userId: string, stream : MediaStream){ addStreamRemoteScreenSharing(userId: string, stream : MediaStream){
// In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet // In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet
const remoteVideo = this.remoteVideo.get(`screen-sharing-${userId}`); const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId));
if (remoteVideo === undefined) { if (remoteVideo === undefined) {
this.addScreenSharingActiveVideo(userId); this.addScreenSharingActiveVideo(userId);
} }
this.addStreamRemoteVideo(`screen-sharing-${userId}`, stream); this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream);
} }
removeActiveVideo(userId: string){ removeActiveVideo(userId: string){
@ -581,7 +598,7 @@ export class MediaManager {
this.removeParticipant(userId); this.removeParticipant(userId);
} }
removeActiveScreenSharingVideo(userId: string) { removeActiveScreenSharingVideo(userId: string) {
this.removeActiveVideo(`screen-sharing-${userId}`) this.removeActiveVideo(this.getScreenSharingId(userId))
} }
playWebrtcOutSound(): void { playWebrtcOutSound(): void {
@ -617,7 +634,7 @@ export class MediaManager {
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
} }
isErrorScreenSharing(userId: string): void { isErrorScreenSharing(userId: string): void {
this.isError(`screen-sharing-${userId}`); this.isError(this.getScreenSharingId(userId));
} }
@ -645,65 +662,8 @@ export class MediaManager {
return color; return color;
} }
public showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){ public addNewParticipant(userId: number|string, name: string|undefined, img?: string, showReportUserCallBack?: ShowReportCallBack){
//create report text area discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
const divReport = document.createElement('div');
divReport.classList.add('modal-report-user');
const inputHidden = document.createElement('input');
inputHidden.id = 'input-report-user';
inputHidden.type = 'hidden';
inputHidden.value = userId;
divReport.appendChild(inputHidden);
const titleMessage = document.createElement('p');
titleMessage.id = 'title-report-user';
titleMessage.innerText = 'Open a report';
divReport.appendChild(titleMessage);
const bodyMessage = document.createElement('p');
bodyMessage.id = 'body-report-user';
bodyMessage.innerText = `You are about to open a report regarding an offensive conduct from user ${userName.toUpperCase()}. Please explain to us how you think ${userName.toUpperCase()} breached the code of conduct.`;
divReport.appendChild(bodyMessage);
const imgReportUser = document.createElement('img');
imgReportUser.id = 'img-report-user';
imgReportUser.src = 'resources/logos/report.svg';
divReport.appendChild(imgReportUser);
const textareaUser = document.createElement('textarea');
textareaUser.id = 'textarea-report-user';
textareaUser.placeholder = 'Write ...';
divReport.appendChild(textareaUser);
const buttonReport = document.createElement('button');
buttonReport.id = 'button-save-report-user';
buttonReport.innerText = 'Report';
buttonReport.addEventListener('click', () => {
if(!textareaUser.value){
textareaUser.style.border = '1px solid red'
return;
}
reportCallBack(textareaUser.value);
divReport.remove();
});
divReport.appendChild(buttonReport);
const buttonCancel = document.createElement('img');
buttonCancel.id = 'cancel-report-user';
buttonCancel.src = 'resources/logos/close.svg';
buttonCancel.addEventListener('click', () => {
divReport.remove();
});
divReport.appendChild(buttonCancel);
mainContainer.appendChild(divReport);
}
public addNewParticipant(userId: number|string, name: string|undefined, img?: string, reportCallBack?: ReportCallback){
discussionManager.addParticipant(userId, name, img, false, reportCallBack);
} }
public removeParticipant(userId: number|string){ public removeParticipant(userId: number|string){
@ -769,6 +729,10 @@ export class MediaManager {
this.checkActiveUser(); this.checkActiveUser();
}, this.focused ? 10000 : 1000); }, this.focused ? 10000 : 1000);
} }
public setShowReportModalCallBacks(callback: ShowReportCallBack){
this.showReportModalCallBacks.add(callback);
}
} }
export const mediaManager = new MediaManager(); export const mediaManager = new MediaManager();

View File

@ -1,8 +1,9 @@
import * as SimplePeerNamespace from "simple-peer"; import * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager"; import {mediaManager} from "./MediaManager";
import {TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable"; import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection"; import {RoomConnection} from "../Connexion/RoomConnection";
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer"; import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
import {UserSimplePeerInterface} from "./SimplePeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
@ -16,25 +17,28 @@ export class ScreenSharingPeer extends Peer {
private isReceivingStream:boolean = false; private isReceivingStream:boolean = false;
public toClose: boolean = false; public toClose: boolean = false;
public _connected: boolean = false; public _connected: boolean = false;
private userId: number;
constructor(private userId: number, initiator: boolean, private connection: RoomConnection) { constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection) {
super({ super({
initiator: initiator ? initiator : false, initiator: initiator ? initiator : false,
reconnectTimer: 10000, reconnectTimer: 10000,
config: { config: {
iceServers: [ iceServers: [
{ {
urls: 'stun:stun.l.google.com:19302' urls: STUN_SERVER.split(',')
}, },
{ TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','), urls: TURN_SERVER.split(','),
username: TURN_USER, username: user.webRtcUser || TURN_USER,
credential: TURN_PASSWORD credential: user.webRtcPassword || TURN_PASSWORD
}, } : undefined,
] ].filter((value) => value !== undefined)
} }
}); });
this.userId = user.userId;
//start listen signal for the peer connection //start listen signal for the peer connection
this.on('signal', (data: unknown) => { this.on('signal', (data: unknown) => {
this.sendWebrtcScreenSharingSignal(data); this.sendWebrtcScreenSharingSignal(data);

View File

@ -9,13 +9,18 @@ import {
UpdatedLocalStreamCallback UpdatedLocalStreamCallback
} from "./MediaManager"; } from "./MediaManager";
import {ScreenSharingPeer} from "./ScreenSharingPeer"; import {ScreenSharingPeer} from "./ScreenSharingPeer";
import {MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
import {RoomConnection} from "../Connexion/RoomConnection"; import {RoomConnection} from "../Connexion/RoomConnection";
import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
import {blackListManager} from "./BlackListManager";
export interface UserSimplePeerInterface{ export interface UserSimplePeerInterface{
userId: number; userId: number;
name?: string; name?: string;
initiator?: boolean; initiator?: boolean;
webRtcUser?: string|undefined;
webRtcPassword?: string|undefined;
} }
export interface PeerConnectionListener { export interface PeerConnectionListener {
@ -36,6 +41,7 @@ export class SimplePeer {
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback; private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback; private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>(); private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number;
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) { constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
// We need to go through this weird bound function pointer in order to be able to "free" this reference later. // We need to go through this weird bound function pointer in order to be able to "free" this reference later.
@ -46,6 +52,7 @@ export class SimplePeer {
mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback); mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback);
mediaManager.onStartScreenSharing(this.sendLocalScreenSharingStreamCallback); mediaManager.onStartScreenSharing(this.sendLocalScreenSharingStreamCallback);
mediaManager.onStopScreenSharing(this.stopLocalScreenSharingStreamCallback); mediaManager.onStopScreenSharing(this.stopLocalScreenSharingStreamCallback);
this.userId = Connection.getUserId();
this.initialise(); this.initialise();
} }
@ -89,15 +96,14 @@ export class SimplePeer {
}); });
} }
private receiveWebrtcStart(user: UserSimplePeerInterface) { private receiveWebrtcStart(user: UserSimplePeerInterface): void {
//this.WebRtcRoomId = data.roomId;
this.Users.push(user); this.Users.push(user);
// Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group) // Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group)
// So we can receive a request we already had before. (which will abort at the first line of createPeerConnection) // So we can receive a request we already had before. (which will abort at the first line of createPeerConnection)
// This would be symmetrical to the way we handle disconnection. // This would be symmetrical to the way we handle disconnection.
//start connection //start connection
console.log('receiveWebrtcStart. Initiator: ', user.initiator) //console.log('receiveWebrtcStart. Initiator: ', user.initiator)
if(!user.initiator){ if(!user.initiator){
return; return;
} }
@ -134,17 +140,13 @@ export class SimplePeer {
mediaManager.removeActiveVideo("" + user.userId); mediaManager.removeActiveVideo("" + user.userId);
const reportCallback = this.enableReporting ? (comment: string) => { mediaManager.addActiveVideo(user, name);
this.reportUser(user.userId, comment);
} : undefined;
mediaManager.addActiveVideo("" + user.userId, reportCallback, name); const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection);
const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
//permit to send message //permit to send message
mediaManager.addSendMessageCallback(user.userId,(message: string) => { mediaManager.addSendMessageCallback(user.userId,(message: string) => {
peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), message: message}))); peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), userId: this.userId, message: message})));
}); });
peer.toClose = false; peer.toClose = false;
@ -189,7 +191,7 @@ export class SimplePeer {
mediaManager.addScreenSharingActiveVideo("" + user.userId); mediaManager.addScreenSharingActiveVideo("" + user.userId);
} }
const peer = new ScreenSharingPeer(user.userId, user.initiator ? user.initiator : false, this.Connection); const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection);
this.PeerScreenSharingConnectionArray.set(user.userId, peer); this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) { for (const peerConnectionListener of this.peerConnectionListeners) {
@ -300,6 +302,7 @@ export class SimplePeer {
} }
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) { private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
if (blackListManager.isBlackListed(data.userId)) return;
console.log("receiveWebrtcScreenSharingSignal", data); console.log("receiveWebrtcScreenSharingSignal", data);
try { try {
//if offer type, create peer connection //if offer type, create peer connection
@ -391,14 +394,8 @@ export class SimplePeer {
} }
} }
/**
* Triggered locally when clicking on the report button
*/
public reportUser(userId: number, message: string) {
this.Connection.emitReportPlayerMessage(userId, message)
}
private sendLocalScreenSharingStreamToUser(userId: number): void { private sendLocalScreenSharingStreamToUser(userId: number): void {
if (blackListManager.isBlackListed(userId)) return;
// If a connection already exists with user (because it is already sharing a screen with us... let's use this connection) // If a connection already exists with user (because it is already sharing a screen with us... let's use this connection)
if (this.PeerScreenSharingConnectionArray.has(userId)) { if (this.PeerScreenSharingConnectionArray.has(userId)) {
this.pushScreenSharingToRemoteUser(userId); this.pushScreenSharingToRemoteUser(userId);

View File

@ -1,65 +1,57 @@
import * as SimplePeerNamespace from "simple-peer"; import * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager"; import {mediaManager} from "./MediaManager";
import {TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable"; import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection"; import {RoomConnection} from "../Connexion/RoomConnection";
import {blackListManager} from "./BlackListManager";
import {Subscription} from "rxjs";
import {UserSimplePeerInterface} from "./SimplePeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
export const MESSAGE_TYPE_CONSTRAINT = 'constraint'; export const MESSAGE_TYPE_CONSTRAINT = 'constraint';
export const MESSAGE_TYPE_MESSAGE = 'message'; export const MESSAGE_TYPE_MESSAGE = 'message';
export const MESSAGE_TYPE_BLOCKED = 'blocked';
export const MESSAGE_TYPE_UNBLOCKED = 'unblocked';
/** /**
* A peer connection used to transmit video / audio signals between 2 peers. * A peer connection used to transmit video / audio signals between 2 peers.
*/ */
export class VideoPeer extends Peer { export class VideoPeer extends Peer {
public toClose: boolean = false; public toClose: boolean = false;
public _connected: boolean = false; public _connected: boolean = false;
private remoteStream!: MediaStream;
private blocked: boolean = false;
private userId: number;
private userName: string;
private onBlockSubscribe: Subscription;
private onUnBlockSubscribe: Subscription;
constructor(public userId: number, initiator: boolean, private connection: RoomConnection) { constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection) {
super({ super({
initiator: initiator ? initiator : false, initiator: initiator ? initiator : false,
reconnectTimer: 10000, reconnectTimer: 10000,
config: { config: {
iceServers: [ iceServers: [
{ {
urls: 'stun:stun.l.google.com:19302' urls: STUN_SERVER.split(',')
}, },
{ TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','), urls: TURN_SERVER.split(','),
username: TURN_USER, username: user.webRtcUser || TURN_USER,
credential: TURN_PASSWORD credential: user.webRtcPassword || TURN_PASSWORD
}, } : undefined,
] ].filter((value) => value !== undefined)
} }
}); });
console.log('PEER SETUP ', { this.userId = user.userId;
initiator: initiator ? initiator : false, this.userName = user.name || '';
reconnectTimer: 10000,
config: {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: TURN_SERVER.split(','),
username: TURN_USER,
credential: TURN_PASSWORD
},
]
}
});
//start listen signal for the peer connection //start listen signal for the peer connection
this.on('signal', (data: unknown) => { this.on('signal', (data: unknown) => {
this.sendWebrtcSignal(data); this.sendWebrtcSignal(data);
}); });
this.on('stream', (stream: MediaStream) => { this.on('stream', (stream: MediaStream) => this.stream(stream));
this.stream(stream);
});
/*peer.on('track', (track: MediaStreamTrack, stream: MediaStream) => {
});*/
this.on('close', () => { this.on('close', () => {
this._connected = false; this._connected = false;
@ -70,7 +62,7 @@ export class VideoPeer extends Peer {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => { this.on('error', (err: any) => {
console.error(`error => ${this.userId} => ${err.code}`, err); console.error(`error => ${this.userId} => ${err.code}`, err);
mediaManager.isError("" + userId); mediaManager.isError("" + this.userId);
}); });
this.on('connect', () => { this.on('connect', () => {
@ -81,8 +73,6 @@ export class VideoPeer extends Peer {
this.on('data', (chunk: Buffer) => { this.on('data', (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString('utf8'));
console.log("data", message);
if(message.type === MESSAGE_TYPE_CONSTRAINT) { if(message.type === MESSAGE_TYPE_CONSTRAINT) {
if (message.audio) { if (message.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId); mediaManager.enabledMicrophoneByUserId(this.userId);
@ -95,9 +85,20 @@ export class VideoPeer extends Peer {
} else { } else {
mediaManager.disabledVideoByUserId(this.userId); mediaManager.disabledVideoByUserId(this.userId);
} }
} else if(message.type === 'message') { } else if(message.type === MESSAGE_TYPE_MESSAGE) {
if (!blackListManager.isBlackListed(message.userId)) {
mediaManager.addNewMessage(message.name, message.message); mediaManager.addNewMessage(message.name, message.message);
} }
} else if(message.type === MESSAGE_TYPE_BLOCKED) {
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
// Find a way to block A's output stream in A's js client
//However, the output stream stream B is correctly blocked in A client
this.blocked = true;
this.toggleRemoteStream(false);
} else if(message.type === MESSAGE_TYPE_UNBLOCKED) {
this.blocked = false;
this.toggleRemoteStream(true);
}
}); });
this.once('finish', () => { this.once('finish', () => {
@ -105,6 +106,31 @@ export class VideoPeer extends Peer {
}); });
this.pushVideoToRemoteUser(); this.pushVideoToRemoteUser();
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userId) => {
if (userId === this.userId) {
this.toggleRemoteStream(false);
this.sendBlockMessage(true);
}
});
this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userId) => {
if (userId === this.userId) {
this.toggleRemoteStream(true);
this.sendBlockMessage(false);
}
});
if (blackListManager.isBlackListed(this.userId)) {
this.sendBlockMessage(true)
}
}
private sendBlockMessage(blocking: boolean) {
this.write(new Buffer(JSON.stringify({type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED, name: this.userName.toUpperCase(), userId: this.userId, message: ''})));
}
private toggleRemoteStream(enable: boolean) {
this.remoteStream.getTracks().forEach(track => track.enabled = enable);
mediaManager.toggleBlockLogo(this.userId, !enable);
} }
private sendWebrtcSignal(data: unknown) { private sendWebrtcSignal(data: unknown) {
@ -120,13 +146,13 @@ export class VideoPeer extends Peer {
*/ */
private stream(stream: MediaStream) { private stream(stream: MediaStream) {
try { try {
this.remoteStream = stream;
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
this.toggleRemoteStream(false);
}
mediaManager.addStreamRemoteVideo("" + this.userId, stream); mediaManager.addStreamRemoteVideo("" + this.userId, stream);
}catch (err){ }catch (err){
console.error(err); console.error(err);
//Force add streem video
/*setTimeout(() => {
this.stream(stream);
}, 500);*/ //todo: find a way to prevent infinite regression.
} }
} }
@ -139,6 +165,8 @@ export class VideoPeer extends Peer {
if(!this.toClose){ if(!this.toClose){
return; return;
} }
this.onBlockSubscribe.unsubscribe();
this.onUnBlockSubscribe.unsubscribe();
mediaManager.removeActiveVideo("" + this.userId); mediaManager.removeActiveVideo("" + this.userId);
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.

View File

@ -1,7 +1,6 @@
import 'phaser'; import 'phaser';
import GameConfig = Phaser.Types.Core.GameConfig; import GameConfig = Phaser.Types.Core.GameConfig;
import {DEBUG_MODE, JITSI_URL, RESOLUTION} from "./Enum/EnvironmentVariable"; import {DEBUG_MODE, JITSI_URL, RESOLUTION} from "./Enum/EnvironmentVariable";
import {cypressAsserter} from "./Cypress/CypressAsserter";
import {LoginScene} from "./Phaser/Login/LoginScene"; import {LoginScene} from "./Phaser/Login/LoginScene";
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene"; import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene"; import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene";
@ -53,8 +52,28 @@ const fps : Phaser.Types.Core.FPSConfig = {
smoothStep: false smoothStep: false
} }
// the ?phaserMode=canvas parameter can be used to force Canvas usage
const params = new URLSearchParams(document.location.search.substring(1));
const phaserMode = params.get("phaserMode");
let mode: number;
switch (phaserMode) {
case 'auto':
case null:
mode = Phaser.AUTO;
break;
case 'canvas':
mode = Phaser.CANVAS;
break;
case 'webgl':
mode = Phaser.WEBGL;
break;
default:
throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"');
}
const config: GameConfig = { const config: GameConfig = {
type: Phaser.AUTO, type: mode,
title: "WorkAdventure", title: "WorkAdventure",
width: width / RESOLUTION, width: width / RESOLUTION,
height: height / RESOLUTION, height: height / RESOLUTION,
@ -65,6 +84,11 @@ const config: GameConfig = {
dom: { dom: {
createContainer: true createContainer: true
}, },
render: {
pixelArt: true,
roundPixels: true,
antialias: false
},
physics: { physics: {
default: "arcade", default: "arcade",
arcade: { arcade: {
@ -73,16 +97,15 @@ const config: GameConfig = {
}, },
callbacks: { callbacks: {
postBoot: game => { postBoot: game => {
const renderer = game.renderer; // Commented out to try to fix MacOS bug
/*const renderer = game.renderer;
if (renderer instanceof WebGLRenderer) { if (renderer instanceof WebGLRenderer) {
renderer.pipelines.add(OutlinePipeline.KEY, new OutlinePipeline(game)); renderer.pipelines.add(OutlinePipeline.KEY, new OutlinePipeline(game));
} }*/
} }
} }
}; };
cypressAsserter.gameStarted();
const game = new Phaser.Game(config); const game = new Phaser.Game(config);
window.addEventListener('resize', function (event) { window.addEventListener('resize', function (event) {

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -x set -x
set -o nounset errexit set -o nounset errexit
template_file_index=dist/index.html.tmpl template_file_index=dist/index.tmpl.html
generated_file_index=dist/index.html generated_file_index=dist/index.html
tmp_trackcodefile=/tmp/trackcode tmp_trackcodefile=/tmp/trackcode

View File

@ -2,13 +2,19 @@ import "jasmine";
import {HtmlUtils} from "../../../src/WebRtc/HtmlUtils"; import {HtmlUtils} from "../../../src/WebRtc/HtmlUtils";
describe("urlify()", () => { describe("urlify()", () => {
it("should transform an url into a link", () => { // FIXME: we need to add PhantomJS to have a good mock for "document".
const text = HtmlUtils.urlify('https://google.com'); /*it("should transform an url into a link", () => {
expect(text).toEqual('<a href="https://google.com" target="_blank" style=":visited {color: white}">https://google.com</a>'); const text = HtmlUtils.urlify('foo https://google.com bar');
expect(text).toEqual('foo <a href="https://google.com" target="_blank" style=":visited {color: white}">https://google.com</a> bar');
}); });
it("should not transform a normal text into a link", () => { it("should not transform a normal text into a link", () => {
const text = HtmlUtils.urlify('hello'); const text = HtmlUtils.urlify('hello');
expect(text).toEqual('hello'); expect(text).toEqual('hello');
}); });
it("should escape HTML", () => {
const text = HtmlUtils.urlify('<h1>boo</h1>');
expect(text).toEqual('&lt;h1&gt;boo&lt;/h1&gt;');
});*/
}); });

View File

@ -39,13 +39,34 @@ module.exports = {
plugins: [ plugins: [
new HtmlWebpackPlugin( new HtmlWebpackPlugin(
{ {
template: './dist/index.html' template: './dist/index.tmpl.html',
minify: {
collapseWhitespace: true,
keepClosingSlash: true,
removeComments: false,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
}
} }
), ),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Phaser: 'phaser' Phaser: 'phaser'
}), }),
new webpack.EnvironmentPlugin(['API_URL', 'UPLOADER_URL', 'ADMIN_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL', 'JITSI_PRIVATE_MODE', 'START_ROOM_URL']) new webpack.EnvironmentPlugin([
'API_URL',
'UPLOADER_URL',
'ADMIN_URL',
'DEBUG_MODE',
'STUN_SERVER',
'TURN_SERVER',
'TURN_USER',
'TURN_PASSWORD',
'JITSI_URL',
'JITSI_PRIVATE_MODE',
'START_ROOM_URL'
])
], ],
}; };

View File

@ -1771,7 +1771,7 @@ eventemitter3@^2.0.3:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo= integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=
eventemitter3@^4.0.0, eventemitter3@^4.0.7: eventemitter3@^4.0.0, eventemitter3@^4.0.4:
version "4.0.7" version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
@ -1829,7 +1829,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
dependencies: dependencies:
homedir-polyfill "^1.0.1" homedir-polyfill "^1.0.1"
exports-loader@^1.1.1: exports-loader@^1.1.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-1.1.1.tgz#88c9a6877ee6a5519d7c41a016bdd99148421e69" resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-1.1.1.tgz#88c9a6877ee6a5519d7c41a016bdd99148421e69"
integrity sha512-CmyhIR2sJ3KOfVsHjsR0Yvo+0lhRhRMAevCbB8dhTVLHsZPs0lCQTvRmR9YNvBXDBxUuhmCE2f54KqEjZUaFrg== integrity sha512-CmyhIR2sJ3KOfVsHjsR0Yvo+0lhRhRMAevCbB8dhTVLHsZPs0lCQTvRmR9YNvBXDBxUuhmCE2f54KqEjZUaFrg==
@ -2528,7 +2528,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0" pkg-dir "^3.0.0"
resolve-cwd "^2.0.0" resolve-cwd "^2.0.0"
imports-loader@^1.2.0: imports-loader@^1.1.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-1.2.0.tgz#b06823d0bb42e6f5ff89bc893829000eda46693f" resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-1.2.0.tgz#b06823d0bb42e6f5ff89bc893829000eda46693f"
integrity sha512-zPvangKEgrrPeqeUqH0Uhc59YqK07JqZBi9a9cQ3v/EKUIqrbJHY4CvUrDus2lgQa5AmPyXuGrWP8JJTqzE5RQ== integrity sha512-zPvangKEgrrPeqeUqH0Uhc59YqK07JqZBi9a9cQ3v/EKUIqrbJHY4CvUrDus2lgQa5AmPyXuGrWP8JJTqzE5RQ==
@ -3663,14 +3663,14 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
phaser@^3.52.0: phaser@3.24.1:
version "3.52.0" version "3.24.1"
resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.52.0.tgz#834dd7a7717308c2576d2450d0806317cf3d1ea8" resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.24.1.tgz#376e0c965d2a35af37c06ee78627dafbde5be017"
integrity sha512-oH39AUXR+cletB3GIvLnVO2J7/M6xs0D0LamVdCrbCDO3jckQOVBcqR6SJ2wam+4D5iJTCXv8K+FmxFzcTUWuA== integrity sha512-WbrRMkbpEzarkfrq83akeauc6b8xNxsOTpDygyW7wrU2G2ne6kOYu3hji4UAaGnZaOLrVuj8ycYPjX9P1LxcDw==
dependencies: dependencies:
eventemitter3 "^4.0.7" eventemitter3 "^4.0.4"
exports-loader "^1.1.1" exports-loader "^1.1.0"
imports-loader "^1.2.0" imports-loader "^1.1.0"
path "^0.12.7" path "^0.12.7"
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
@ -4094,7 +4094,7 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies: dependencies:
aproba "^1.1.1" aproba "^1.1.1"
rxjs@^6.6.0: rxjs@^6.6.0, rxjs@^6.6.3:
version "6.6.3" version "6.6.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==

4
maps/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
/dist/bundle.js
/yarn-error.log
/Dockerfile

View File

@ -298,7 +298,7 @@
{ {
"name":"exitUrl", "name":"exitUrl",
"type":"string", "type":"string",
"value":"\/@\/tcm\/workadventure\/floor1" "value":"..\/Floor1\/floor1.json"
}], }],
"type":"tilelayer", "type":"tilelayer",
"visible":true, "visible":true,

View File

@ -83,9 +83,9 @@
"opacity":1, "opacity":1,
"properties":[ "properties":[
{ {
"name":"exitSceneUrl", "name":"exitUrl",
"type":"string", "type":"string",
"value":"\/@\/tcm\/workadventure\/floor0#down-the-stairs" "value":"..\/Floor0\/floor0.json"
}], }],
"type":"tilelayer", "type":"tilelayer",
"visible":true, "visible":true,
@ -264,7 +264,7 @@
"nextobjectid":1, "nextobjectid":1,
"orientation":"orthogonal", "orientation":"orthogonal",
"renderorder":"right-down", "renderorder":"right-down",
"tiledversion":"1.4.2", "tiledversion":"1.3.3",
"tileheight":32, "tileheight":32,
"tilesets":[ "tilesets":[
{ {
@ -1959,6 +1959,6 @@
}], }],
"tilewidth":32, "tilewidth":32,
"type":"map", "type":"map",
"version":1.4, "version":1.2,
"width":46 "width":46
} }

View File

@ -0,0 +1,25 @@
License
-------
CC-BY-SA 3.0:
- http://creativecommons.org/licenses/by-sa/3.0/
- See the file: cc-by-sa-3.0.txt
GNU GPL 3.0:
- http://www.gnu.org/licenses/gpl-3.0.html
- See the file: gpl-3.0.txt
Assets from: workadventure@thecodingmachine.com
BASE assets:
------------
- le-coq.png
- logotcm.png
- pin.png
- tileset1-repositioning.png
- tileset1.png
- tileset2.2.png
- tileset2.png
- tileset3.2.png
- tileset3.png
- walls2.png

View File

@ -0,0 +1,99 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":4,
"name":"jitsi",
"opacity":1,
"properties":[
{
"name":"jitsiConfig",
"type":"string",
"value":"{ \"startWithAudioMuted\": true }"
},
{
"name":"jitsiInterfaceConfig",
"type":"string",
"value":"{ \"DEFAULT_BACKGROUND\": \"#77ee77\" }"
},
{
"name":"jitsiRoom",
"type":"string",
"value":"myRoom avec espace \u00e9\u00e0$&'\"_ \ud83d\ude00"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.3.3",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.2,
"width":10
}

BIN
maps/tests/tileset1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

4
messages/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
/dist/bundle.js
/yarn-error.log
/Dockerfile

View File

@ -168,6 +168,8 @@ message WebRtcStartMessage {
int32 userId = 1; int32 userId = 1;
string name = 2; string name = 2;
bool initiator = 3; bool initiator = 3;
string webrtcUserName = 4;
string webrtcPassword = 5;
} }
message WebRtcDisconnectMessage { message WebRtcDisconnectMessage {
@ -177,6 +179,8 @@ message WebRtcDisconnectMessage {
message WebRtcSignalToClientMessage { message WebRtcSignalToClientMessage {
int32 userId = 1; int32 userId = 1;
string signal = 2; string signal = 2;
string webrtcUserName = 4;
string webrtcPassword = 5;
} }
message TeleportMessageMessage{ message TeleportMessageMessage{

View File

@ -1,15 +1,26 @@
FROM thecodingmachine/workadventure-back-base:latest as builder # protobuf build
WORKDIR /var/www/messages FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
COPY --chown=docker:docker messages . WORKDIR /usr/src
COPY messages .
RUN yarn install && yarn proto RUN yarn install && yarn proto
FROM thecodingmachine/nodejs:12 # typescript build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
COPY --chown=docker:docker pusher . WORKDIR /usr/src
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated COPY pusher/yarn.lock pusher/package.json ./
RUN yarn install RUN yarn install
COPY pusher .
COPY --from=builder /usr/src/generated src/Messages/generated
ENV NODE_ENV=production ENV NODE_ENV=production
RUN yarn run tsc RUN yarn run tsc
# final production image
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76
WORKDIR /usr/src
COPY pusher/yarn.lock pusher/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", "runprod"]

17
pusher/Dockerfile.prod Normal file
View File

@ -0,0 +1,17 @@
FROM node:12.19.0-slim
RUN mkdir -p /home/node/app && chown -R node:node /home/node/app
WORKDIR /home/node/app
USER node
ENV NODE_ENV=production
ENV DEBUG=*
COPY --chown=node:node package.json yarn.lock ./
RUN yarn install --prod --frozen-lockfile
COPY --chown=node:node ./dist/ ./dist/
EXPOSE 8080
CMD ["yarn", "run", "runprod"]

View File

@ -105,11 +105,12 @@ class AdminApi {
return res.data; return res.data;
} }
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) { reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) {
return Axios.post(`${ADMIN_API_URL}/api/report`, { return Axios.post(`${ADMIN_API_URL}/api/report`, {
reportedUserUuid, reportedUserUuid,
reportedUserComment, reportedUserComment,
reporterUserUuid, reporterUserUuid,
reportWorldSlug
}, },
{ {
headers: {"Authorization": `${ADMIN_API_TOKEN}`} headers: {"Authorization": `${ADMIN_API_TOKEN}`}

View File

@ -172,6 +172,7 @@ export class SocketManager implements ZoneEventListener {
console.log('Calling joinRoom') console.log('Calling joinRoom')
const apiClient = await apiClientRepository.getClient(client.roomId); const apiClient = await apiClientRepository.getClient(client.roomId);
const streamToPusher = apiClient.joinRoom(); const streamToPusher = apiClient.joinRoom();
clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId);
client.backConnection = streamToPusher; client.backConnection = streamToPusher;
@ -304,7 +305,7 @@ export class SocketManager implements ZoneEventListener {
throw 'reported socket user not found'; throw 'reported socket user not found';
} }
//TODO report user on admin application //TODO report user on admin application
await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid) await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2])
} catch (e) { } catch (e) {
console.error('An error occurred on "handleReportMessage"'); console.error('An error occurred on "handleReportMessage"');
console.error(e); console.error(e);

View File

@ -1,9 +1,19 @@
FROM thecodingmachine/nodejs:12 # typescript build
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
COPY --chown=docker:docker uploader . WORKDIR /usr/src
COPY uploader/yarn.lock uploader/package.json ./
RUN yarn install RUN yarn install
COPY uploader .
ENV NODE_ENV=production ENV NODE_ENV=production
RUN yarn run tsc RUN yarn run tsc
# final production image
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76
WORKDIR /usr/src
COPY uploader/yarn.lock uploader/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", "runprod"]

5
website/.dockerignore Normal file
View File

@ -0,0 +1,5 @@
/dist/
/node_modules/
/dist/bundle.js
/yarn-error.log
/Dockerfile