Merge pull request #754 from thecodingmachine/develop
Deploy 2021-02-16
This commit is contained in:
commit
36a713ec91
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
**/node_modules/**
|
||||
**/Dockerfile
|
@ -6,3 +6,7 @@ JITSI_ISS=
|
||||
SECRET_JITSI_KEY=
|
||||
ADMIN_API_TOKEN=123
|
||||
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=
|
||||
|
3
.github/workflows/build-and-deploy.yml
vendored
3
.github/workflows/build-and-deploy.yml
vendored
@ -150,6 +150,7 @@ jobs:
|
||||
JITSI_ISS: ${{ secrets.JITSI_ISS }}
|
||||
JITSI_URL: ${{ secrets.JITSI_URL }}
|
||||
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
||||
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
|
||||
with:
|
||||
namespace: workadventure-${{ env.GITHUB_REF_SLUG }}
|
||||
|
||||
@ -159,5 +160,5 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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
|
||||
|
10
README.md
10
README.md
@ -6,8 +6,6 @@ Demo here : [https://workadventu.re/](https://workadventu.re/).
|
||||
|
||||
# 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
|
||||
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).
|
||||
|
||||
|
||||
## Getting started
|
||||
## Setting up a development environment
|
||||
|
||||
Install Docker.
|
||||
|
||||
@ -101,5 +99,7 @@ Vagrant destroy
|
||||
* `Vagrant halt`: stop your VM Vagrant.
|
||||
* `Vagrant destroy`: delete your VM Vagrant.
|
||||
|
||||
## Features developed
|
||||
You have more details of features developed in back [README.md](./back/README.md).
|
||||
## Setting up a production environment
|
||||
|
||||
The way you set up your production environment will highly depend on your servers.
|
||||
We provide a production ready `docker-compose` file that you can use as a good starting point in the [contrib/docker](https://github.com/thecodingmachine/workadventure/tree/master/contrib/docker) directory.
|
||||
|
@ -1,16 +1,26 @@
|
||||
FROM thecodingmachine/workadventure-back-base:latest as builder
|
||||
WORKDIR /var/www/messages
|
||||
COPY --chown=docker:docker messages .
|
||||
# protobuf build
|
||||
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
|
||||
WORKDIR /usr/src
|
||||
COPY messages .
|
||||
RUN yarn install && yarn proto
|
||||
|
||||
FROM thecodingmachine/nodejs:12
|
||||
|
||||
COPY --chown=docker:docker back .
|
||||
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated
|
||||
# typescript build
|
||||
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
|
||||
WORKDIR /usr/src
|
||||
COPY back/yarn.lock back/package.json ./
|
||||
RUN yarn install
|
||||
|
||||
COPY back .
|
||||
COPY --from=builder /usr/src/generated src/Messages/generated
|
||||
ENV NODE_ENV=production
|
||||
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"]
|
||||
|
@ -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)
|
@ -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 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;
|
||||
@ -12,9 +11,9 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
|
||||
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080;
|
||||
const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051;
|
||||
export const 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 {
|
||||
SECRET_KEY,
|
||||
MINIMUM_DISTANCE,
|
||||
ADMIN_API_URL,
|
||||
ADMIN_API_TOKEN,
|
||||
|
@ -28,7 +28,13 @@ import {User, UserSocket} from "../Model/User";
|
||||
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
|
||||
import {Group} from "../Model/Group";
|
||||
import {cpuTracker} from "./CpuTracker";
|
||||
import {GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} 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 {PositionInterface} from "../Model/PositionInterface";
|
||||
import {adminApi, CharacterTexture} from "./AdminApi";
|
||||
@ -40,6 +46,8 @@ import {ZoneSocket} from "../RoomManager";
|
||||
import {Zone} from "_Model/Zone";
|
||||
import Debug from "debug";
|
||||
import {Admin} from "_Model/Admin";
|
||||
import crypto from "crypto";
|
||||
|
||||
|
||||
const debug = Debug('sockermanager');
|
||||
|
||||
@ -275,6 +283,12 @@ export class SocketManager {
|
||||
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
|
||||
webrtcSignalToClient.setUserid(user.id);
|
||||
webrtcSignalToClient.setSignal(data.getSignal());
|
||||
// TODO: only compute credentials if data.signal.type === "offer"
|
||||
if (TURN_STATIC_AUTH_SECRET !== '') {
|
||||
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
|
||||
webrtcSignalToClient.setWebrtcusername(username);
|
||||
webrtcSignalToClient.setWebrtcpassword(password);
|
||||
}
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
|
||||
@ -295,6 +309,12 @@ export class SocketManager {
|
||||
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
|
||||
webrtcSignalToClient.setUserid(user.id);
|
||||
webrtcSignalToClient.setSignal(data.getSignal());
|
||||
// TODO: only compute credentials if data.signal.type === "offer"
|
||||
if (TURN_STATIC_AUTH_SECRET !== '') {
|
||||
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
|
||||
webrtcSignalToClient.setWebrtcusername(username);
|
||||
webrtcSignalToClient.setWebrtcpassword(password);
|
||||
}
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
|
||||
@ -487,6 +507,11 @@ export class SocketManager {
|
||||
webrtcStartMessage1.setUserid(otherUser.id);
|
||||
webrtcStartMessage1.setName(otherUser.name);
|
||||
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();
|
||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||
@ -500,6 +525,11 @@ export class SocketManager {
|
||||
webrtcStartMessage2.setUserid(user.id);
|
||||
webrtcStartMessage2.setName(user.name);
|
||||
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();
|
||||
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
|
||||
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
|
||||
|
20
contrib/docker/.env.prod.template
Normal file
20
contrib/docker/.env.prod.template
Normal 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=
|
100
contrib/docker/docker-compose.prod.yaml
Normal file
100
contrib/docker/docker-compose.prod.yaml
Normal 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
|
@ -22,6 +22,7 @@
|
||||
"JITSI_ISS": env.JITSI_ISS,
|
||||
"JITSI_URL": env.JITSI_URL,
|
||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||
} + if adminUrl != null then {
|
||||
"ADMIN_API_URL": adminUrl,
|
||||
} else {}
|
||||
@ -40,6 +41,7 @@
|
||||
"JITSI_ISS": env.JITSI_ISS,
|
||||
"JITSI_URL": env.JITSI_URL,
|
||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||
} + if adminUrl != null then {
|
||||
"ADMIN_API_URL": adminUrl,
|
||||
} else {}
|
||||
|
@ -31,9 +31,12 @@ services:
|
||||
ADMIN_URL: workadventure.localhost
|
||||
STARTUP_COMMAND_1: ./templater.sh
|
||||
STARTUP_COMMAND_2: yarn install
|
||||
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
|
||||
TURN_USER: workadventure
|
||||
TURN_PASSWORD: WorkAdventure123
|
||||
STUN_SERVER: "stun:stun.l.google.com:19302"
|
||||
TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349"
|
||||
# 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"
|
||||
command: yarn run start
|
||||
volumes:
|
||||
@ -108,6 +111,7 @@ services:
|
||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
||||
JITSI_URL: $JITSI_URL
|
||||
JITSI_ISS: $JITSI_ISS
|
||||
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
||||
volumes:
|
||||
- ./back:/usr/src/app
|
||||
labels:
|
||||
@ -149,3 +153,28 @@ services:
|
||||
- ./back:/usr/src/back
|
||||
- ./front:/usr/src/front
|
||||
- ./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
|
||||
|
56
front/dist/resources/html/gameReport.html
vendored
56
front/dist/resources/html/gameReport.html
vendored
@ -12,10 +12,6 @@
|
||||
border-radius: 6px;
|
||||
margin: 2px auto 0;
|
||||
width: 298px;
|
||||
height: 220px;
|
||||
}
|
||||
#gameReport .cautiousText {
|
||||
font-size: 50%;
|
||||
}
|
||||
#gameReport h1 {
|
||||
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
|
||||
@ -30,6 +26,9 @@
|
||||
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);
|
||||
@ -51,15 +50,17 @@
|
||||
}
|
||||
#gameReport button {
|
||||
margin-top: 10px;
|
||||
background-color: black;
|
||||
font-size: 60%;
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border-radius: 7px;
|
||||
padding-bottom: 4px;
|
||||
width: 60px;
|
||||
padding: 3px 10px 3px 10px;
|
||||
}
|
||||
#gameReport button#gameReportFormCancel {
|
||||
background-color: #c7c7c700;
|
||||
color: #292929;
|
||||
display: block;
|
||||
float: right;
|
||||
}
|
||||
#gameReport section a{
|
||||
text-align: center;
|
||||
@ -74,8 +75,11 @@
|
||||
#gameReport section.text-center{
|
||||
text-align: center;
|
||||
}
|
||||
#gameReport section p{
|
||||
#gameReport p{
|
||||
font-size: 8px;
|
||||
margin: 3px 0 0 0;
|
||||
}
|
||||
#gameReport form p{
|
||||
margin: 0px 70px;
|
||||
}
|
||||
#gameReport section p.err{
|
||||
@ -87,18 +91,32 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<form id="gameReport" hidden>
|
||||
<section class="text-center">
|
||||
<h5 id="nameReported"></h5>
|
||||
<input type="hidden" id="idUserReported"/>
|
||||
<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>
|
||||
<h6>Message</h6>
|
||||
<textarea type="text" name="report" id="gameReportInput"></textarea>
|
||||
<p class="err" id="gameReportErr"></p>
|
||||
<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 class="action">
|
||||
<button type="submit" id="gameReportFormSubmit">Submit</button>
|
||||
<button type="submit" id="gameReportFormCancel">Close</button>
|
||||
<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>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
|
3
front/dist/resources/html/gameShare.html
vendored
3
front/dist/resources/html/gameShare.html
vendored
@ -14,9 +14,6 @@
|
||||
width: 298px;
|
||||
height: 150px;
|
||||
}
|
||||
#gameShare .cautiousText {
|
||||
font-size: 50%;
|
||||
}
|
||||
#gameShare h1 {
|
||||
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
|
||||
border-bottom: 1px solid #a6abaf;
|
||||
|
22
front/dist/resources/logos/blockSign.svg
vendored
Normal file
22
front/dist/resources/logos/blockSign.svg
vendored
Normal 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 |
BIN
front/dist/resources/logos/blockingIcon.png
vendored
Normal file
BIN
front/dist/resources/logos/blockingIcon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 111 KiB |
BIN
front/dist/resources/logos/cancel.png
vendored
Normal file
BIN
front/dist/resources/logos/cancel.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
16
front/dist/resources/style/style.css
vendored
16
front/dist/resources/style/style.css
vendored
@ -65,6 +65,12 @@ body .message-info.warning{
|
||||
padding: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
.video-container img.block-logo {
|
||||
left: 30%;
|
||||
bottom: 15%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.video-container button.report{
|
||||
display: block;
|
||||
@ -91,7 +97,7 @@ body .message-info.warning{
|
||||
}
|
||||
|
||||
.video-container button.report:hover {
|
||||
width: 94px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.video-container button.report img{
|
||||
@ -111,6 +117,9 @@ body .message-info.warning{
|
||||
font-size: 16px;
|
||||
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
|
||||
}
|
||||
.video-container img.active {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.video-container video{
|
||||
height: 100%;
|
||||
@ -188,10 +197,7 @@ video#myCamVideo{
|
||||
transition: all .2s;
|
||||
right: 224px;
|
||||
}
|
||||
/*.btn-call{
|
||||
transition: all .1s;
|
||||
left: 0px;
|
||||
}*/
|
||||
|
||||
.btn-cam-action div img{
|
||||
height: 22px;
|
||||
width: 30px;
|
||||
|
@ -29,6 +29,7 @@
|
||||
"phaser": "3.24.1",
|
||||
"queue-typescript": "^1.0.1",
|
||||
"quill": "^1.3.7",
|
||||
"rxjs": "^6.6.3",
|
||||
"simple-peer": "^9.6.2",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"webpack-require-http": "^0.4.3"
|
||||
|
@ -96,7 +96,9 @@ export interface WebRtcSignalSentMessageInterface {
|
||||
|
||||
export interface WebRtcSignalReceivedMessageInterface {
|
||||
userId: number,
|
||||
signal: SignalData
|
||||
signal: SignalData,
|
||||
webRtcUser: string | undefined,
|
||||
webRtcPassword: string | undefined
|
||||
}
|
||||
|
||||
export interface StartMapInterface {
|
||||
|
@ -427,7 +427,9 @@ export class RoomConnection implements RoomConnection {
|
||||
callback({
|
||||
userId: message.getUserid(),
|
||||
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) => {
|
||||
callback({
|
||||
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) => {
|
||||
callback({
|
||||
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;
|
||||
}
|
||||
|
||||
@ -583,7 +590,7 @@ export class RoomConnection implements RoomConnection {
|
||||
public hasTag(tag: string): boolean {
|
||||
return this.tags.includes(tag);
|
||||
}
|
||||
|
||||
|
||||
public isAdmin(): boolean {
|
||||
return this.hasTag('admin');
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? w
|
||||
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 STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
|
||||
const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca";
|
||||
const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com';
|
||||
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$';
|
||||
const TURN_SERVER: string = process.env.TURN_SERVER || "";
|
||||
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_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
|
||||
const RESOLUTION = 2;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import {GameScene} from "../Game/GameScene";
|
||||
import {PointInterface} from "../../Connexion/ConnexionModels";
|
||||
import {Character} from "../Entity/Character";
|
||||
import {Sprite} from "./Sprite";
|
||||
|
||||
/**
|
||||
* 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
|
||||
this.userId = userId;
|
||||
|
||||
//todo: implement on click action
|
||||
/*this.playerName.setInteractive();
|
||||
this.playerName.on('pointerup', () => {
|
||||
});*/
|
||||
}
|
||||
|
||||
updatePosition(position: PointInterface): void {
|
||||
|
@ -14,7 +14,7 @@ export class SpeechBubble {
|
||||
const bubbleWidth = bubblePadding * 2 + text.length * 10;
|
||||
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);
|
||||
|
||||
// Bubble shadow
|
||||
|
@ -716,6 +716,10 @@ export class GameScene extends ResizableScene implements CenterListener {
|
||||
if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey);
|
||||
urlManager.pushStartLayerNameToUrl(hash);
|
||||
if (roomId !== this.scene.key) {
|
||||
if (this.scene.get(roomId) === null) {
|
||||
console.error("next room not loaded", exitKey);
|
||||
return;
|
||||
}
|
||||
this.cleanupClosingScene();
|
||||
this.scene.stop();
|
||||
this.scene.remove(this.scene.key);
|
||||
|
@ -2,17 +2,16 @@ import {LoginScene, LoginSceneName} from "../Login/LoginScene";
|
||||
import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCharacterScene";
|
||||
import {gameManager} from "../Game/GameManager";
|
||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||
import {mediaManager, ReportCallback, ShowReportCallBack} from "../../WebRtc/MediaManager";
|
||||
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
|
||||
import {GameConnexionTypes} from "../../Url/UrlManager";
|
||||
import {mediaManager} from "../../WebRtc/MediaManager";
|
||||
import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu";
|
||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
||||
import {GameConnexionTypes} from "../../Url/UrlManager";
|
||||
|
||||
export const MenuSceneName = 'MenuScene';
|
||||
const gameMenuKey = 'gameMenu';
|
||||
const gameMenuIconKey = 'gameMenuIcon';
|
||||
const gameSettingsMenuKey = 'gameSettingsMenu';
|
||||
const gameShare = 'gameShare';
|
||||
const gameReport = 'gameReport';
|
||||
|
||||
const closedSideMenuX = -200;
|
||||
const openedSideMenuX = 0;
|
||||
@ -24,11 +23,10 @@ export class MenuScene extends Phaser.Scene {
|
||||
private menuElement!: Phaser.GameObjects.DOMElement;
|
||||
private gameQualityMenuElement!: Phaser.GameObjects.DOMElement;
|
||||
private gameShareElement!: Phaser.GameObjects.DOMElement;
|
||||
private gameReportElement!: Phaser.GameObjects.DOMElement;
|
||||
private gameReportElement!: ReportMenu;
|
||||
private sideMenuOpened = false;
|
||||
private settingsMenuOpened = false;
|
||||
private gameShareOpened = false;
|
||||
private gameReportOpened = false;
|
||||
private gameQualityValue: number;
|
||||
private videoQualityValue: number;
|
||||
private menuButton!: Phaser.GameObjects.DOMElement;
|
||||
@ -45,21 +43,21 @@ export class MenuScene extends Phaser.Scene {
|
||||
this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html');
|
||||
this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html');
|
||||
this.load.html(gameShare, 'resources/html/gameShare.html');
|
||||
this.load.html(gameReport, 'resources/html/gameReport.html');
|
||||
this.load.html(gameReportKey, gameReportRessource);
|
||||
}
|
||||
|
||||
create() {
|
||||
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
|
||||
this.menuElement.setOrigin(0);
|
||||
this.revealMenusAfterInit(this.menuElement, 'gameMenu');
|
||||
MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu');
|
||||
|
||||
const middleX = (window.innerWidth / 3) - 298;
|
||||
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.revealMenusAfterInit(this.gameShareElement, gameShare);
|
||||
MenuScene.revealMenusAfterInit(this.gameShareElement, gameShare);
|
||||
this.gameShareElement.addListener('click');
|
||||
this.gameShareElement.on('click', (event:MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@ -70,18 +68,11 @@ export class MenuScene extends Phaser.Scene {
|
||||
}
|
||||
});
|
||||
|
||||
this.gameReportElement = this.add.dom(middleX, -400).createFromCache(gameReport);
|
||||
this.revealMenusAfterInit(this.gameReportElement, gameReport);
|
||||
this.gameReportElement.addListener('click');
|
||||
this.gameReportElement.on('click', (event:MouseEvent) => {
|
||||
event.preventDefault();
|
||||
if((event?.target as HTMLInputElement).id === 'gameReportFormSubmit') {
|
||||
this.submitReport();
|
||||
}else if((event?.target as HTMLInputElement).id === 'gameReportFormCancel') {
|
||||
this.closeGameReport();
|
||||
}
|
||||
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
|
||||
mediaManager.setShowReportModalCallBacks((userId, userName) => {
|
||||
this.closeAll();
|
||||
this.gameReportElement.open(parseInt(userId), userName);
|
||||
});
|
||||
mediaManager.setShowReportModalCallBacks(this.openGameReport.bind(this));
|
||||
|
||||
this.input.keyboard.on('keyup-TAB', () => {
|
||||
this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu();
|
||||
@ -96,7 +87,8 @@ export class MenuScene extends Phaser.Scene {
|
||||
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.
|
||||
//To prevent this, we put a 'hidden' attribute on the root element, we remove it only after the init is done.
|
||||
setTimeout(() => {
|
||||
@ -245,71 +237,6 @@ export class MenuScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
private openGameReport(userId: string, userName: string|undefined){
|
||||
if (this.gameReportOpened) {
|
||||
this.closeGameReport();
|
||||
return;
|
||||
}
|
||||
|
||||
//close all
|
||||
this.closeAll();
|
||||
|
||||
const gameTitleReport = this.gameReportElement.getChildByID('nameReported') as HTMLElement;
|
||||
gameTitleReport.innerText = userName ? `Report user: ${userName}` : 'Report user';
|
||||
const gameIdUserReported = this.gameReportElement.getChildByID('idUserReported') as HTMLInputElement;
|
||||
gameIdUserReported.value = userId;
|
||||
|
||||
this.gameReportOpened = true;
|
||||
let middleY = (window.innerHeight / 3) - (257);
|
||||
if(middleY < 0){
|
||||
middleY = 0;
|
||||
}
|
||||
let middleX = (window.innerWidth / 3) - 298;
|
||||
if(middleX < 0){
|
||||
middleX = 0;
|
||||
}
|
||||
|
||||
gameManager.getCurrentGameScene(this).userInputManager.clearAllKeys();
|
||||
|
||||
this.tweens.add({
|
||||
targets: this.gameReportElement,
|
||||
y: middleY,
|
||||
x: middleX,
|
||||
duration: 1000,
|
||||
ease: 'Power3'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
private closeGameReport(): void{
|
||||
this.gameReportOpened = false;
|
||||
gameManager.getCurrentGameScene(this).userInputManager.initKeyBoardEvent();
|
||||
this.tweens.add({
|
||||
targets: this.gameReportElement,
|
||||
y: -400,
|
||||
duration: 1000,
|
||||
ease: 'Power3'
|
||||
});
|
||||
}
|
||||
|
||||
private submitReport(): void{
|
||||
const gamePError = this.gameReportElement.getChildByID('gameReportErr') as HTMLParagraphElement;
|
||||
gamePError.innerText = '';
|
||||
gamePError.style.display = 'none';
|
||||
const gameTextArea = this.gameReportElement.getChildByID('gameReportInput') as HTMLInputElement;
|
||||
const gameIdUserReported = this.gameReportElement.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).connection.emitReportPlayerMessage(
|
||||
parseInt(gameIdUserReported.value),
|
||||
gameTextArea.value
|
||||
);
|
||||
this.closeGameReport();
|
||||
}
|
||||
|
||||
private onMenuClick(event:MouseEvent) {
|
||||
if((event?.target as HTMLInputElement).classList.contains('not-button')){
|
||||
return;
|
||||
@ -372,6 +299,6 @@ export class MenuScene extends Phaser.Scene {
|
||||
private closeAll(){
|
||||
this.closeGameQualityMenu();
|
||||
this.closeGameShare();
|
||||
this.closeGameReport();
|
||||
this.gameReportElement.close();
|
||||
}
|
||||
}
|
||||
|
119
front/src/Phaser/Menu/ReportMenu.ts
Normal file
119
front/src/Phaser/Menu/ReportMenu.ts
Normal 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();
|
||||
}
|
||||
}
|
24
front/src/WebRtc/BlackListManager.ts
Normal file
24
front/src/WebRtc/BlackListManager.ts
Normal 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();
|
@ -3,10 +3,12 @@ import {mediaManager} from "./MediaManager";
|
||||
import {coWebsiteManager} from "./CoWebsiteManager";
|
||||
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
const defaultConfig = {
|
||||
startWithAudioMuted: !mediaManager.constraintsMedia.audio,
|
||||
startWithVideoMuted: mediaManager.constraintsMedia.video === false,
|
||||
prejoinPageEnabled: false
|
||||
const getDefaultConfig = () => {
|
||||
return {
|
||||
startWithAudioMuted: !mediaManager.constraintsMedia.audio,
|
||||
startWithVideoMuted: mediaManager.constraintsMedia.video === false,
|
||||
prejoinPageEnabled: false
|
||||
}
|
||||
}
|
||||
|
||||
const defaultInterfaceConfig = {
|
||||
@ -59,6 +61,14 @@ class JitsiFactory {
|
||||
|
||||
public start(roomName: string, playerName:string, jwt?: string, config?: object, interfaceConfig?: object): void {
|
||||
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 options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
roomName: roomName,
|
||||
@ -66,7 +76,7 @@ class JitsiFactory {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
parentNode: cowebsiteDiv,
|
||||
configOverwrite: {...defaultConfig, ...config},
|
||||
configOverwrite: {...config, ...getDefaultConfig()},
|
||||
interfaceConfigOverwrite: {...defaultInterfaceConfig, ...interfaceConfig}
|
||||
};
|
||||
if (!options.jwt) {
|
||||
|
@ -3,8 +3,7 @@ import {HtmlUtils} from "./HtmlUtils";
|
||||
import {discussionManager, SendMessageCallback} from "./DiscussionManager";
|
||||
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
|
||||
import {VIDEO_QUALITY_SELECT} from "../Administration/ConsoleGlobalMessageManager";
|
||||
import {connectionManager} from "../Connexion/ConnectionManager";
|
||||
import {GameConnexionTypes} from "../Url/UrlManager";
|
||||
import {UserSimplePeerInterface} from "./SimplePeer";
|
||||
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
const localValueVideo = localStorage.getItem(VIDEO_QUALITY_SELECT);
|
||||
@ -28,7 +27,6 @@ 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: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!)
|
||||
export class MediaManager {
|
||||
localStream: MediaStream|null = null;
|
||||
localScreenCapture: MediaStream|null = null;
|
||||
@ -473,8 +471,9 @@ export class MediaManager {
|
||||
return this.getCamera();
|
||||
}
|
||||
|
||||
addActiveVideo(userId: string, userName: string = "", anonymous: boolean = true){
|
||||
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
|
||||
this.webrtcInAudio.play();
|
||||
const userId = ''+user.userId
|
||||
|
||||
userName = userName.toUpperCase();
|
||||
const color = this.getColorByString(userName);
|
||||
@ -484,22 +483,18 @@ export class MediaManager {
|
||||
<div class="connecting-spinner"></div>
|
||||
<div class="rtc-error" style="display: none"></div>
|
||||
<i id="name-${userId}" style="background-color: ${color};">${userName}</i>
|
||||
<img id="microphone-${userId}" src="resources/logos/microphone-close.svg">
|
||||
` +
|
||||
((anonymous === false)?`
|
||||
<button id="report-${userId}" class="report">
|
||||
<img src="resources/logos/report.svg">
|
||||
<span>Report</span>
|
||||
</button>
|
||||
`:''
|
||||
)
|
||||
+
|
||||
`<video id="${userId}" autoplay></video>
|
||||
<img id="microphone-${userId}" title="mute" src="resources/logos/microphone-close.svg">
|
||||
<button id="report-${userId}" class="report">
|
||||
<img title="report this user" src="resources/logos/report.svg">
|
||||
<span>Report/Block</span>
|
||||
</button>
|
||||
<video id="${userId}" autoplay></video>
|
||||
<img src="resources/logos/blockSign.svg" id="blocking-${userId}" class="block-logo">
|
||||
</div>
|
||||
`;
|
||||
|
||||
layoutManager.add(DivImportance.Normal, userId, html);
|
||||
|
||||
|
||||
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
|
||||
|
||||
//permit to create participant in discussion part
|
||||
@ -510,18 +505,17 @@ export class MediaManager {
|
||||
};
|
||||
this.addNewParticipant(userId, userName, undefined, showReportUser);
|
||||
|
||||
if(!anonymous){
|
||||
const reportBanUserAction: HTMLImageElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>(`report-${userId}`);
|
||||
reportBanUserAction.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
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){
|
||||
|
||||
userId = `screen-sharing-${userId}`;
|
||||
userId = this.getScreenSharingId(userId);
|
||||
const html = `
|
||||
<div id="div-${userId}" class="video-container">
|
||||
<video id="${userId}" autoplay></video>
|
||||
@ -532,7 +526,11 @@ export class MediaManager {
|
||||
|
||||
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
|
||||
}
|
||||
|
||||
|
||||
private getScreenSharingId(userId: string): string {
|
||||
return `screen-sharing-${userId}`;
|
||||
}
|
||||
|
||||
disabledMicrophoneByUserId(userId: number){
|
||||
const element = document.getElementById(`microphone-${userId}`);
|
||||
if(!element){
|
||||
@ -571,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 {
|
||||
const remoteVideo = this.remoteVideo.get(userId);
|
||||
if (remoteVideo === undefined) {
|
||||
@ -580,12 +582,12 @@ export class MediaManager {
|
||||
}
|
||||
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
|
||||
const remoteVideo = this.remoteVideo.get(`screen-sharing-${userId}`);
|
||||
const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId));
|
||||
if (remoteVideo === undefined) {
|
||||
this.addScreenSharingActiveVideo(userId);
|
||||
}
|
||||
|
||||
this.addStreamRemoteVideo(`screen-sharing-${userId}`, stream);
|
||||
this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream);
|
||||
}
|
||||
|
||||
removeActiveVideo(userId: string){
|
||||
@ -596,7 +598,7 @@ export class MediaManager {
|
||||
this.removeParticipant(userId);
|
||||
}
|
||||
removeActiveScreenSharingVideo(userId: string) {
|
||||
this.removeActiveVideo(`screen-sharing-${userId}`)
|
||||
this.removeActiveVideo(this.getScreenSharingId(userId))
|
||||
}
|
||||
|
||||
playWebrtcOutSound(): void {
|
||||
@ -632,7 +634,7 @@ export class MediaManager {
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
isErrorScreenSharing(userId: string): void {
|
||||
this.isError(`screen-sharing-${userId}`);
|
||||
this.isError(this.getScreenSharingId(userId));
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ import {mediaManager} from "./MediaManager";
|
||||
import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
|
||||
import {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
|
||||
import {UserSimplePeerInterface} from "./SimplePeer";
|
||||
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||
|
||||
@ -16,8 +17,9 @@ export class ScreenSharingPeer extends Peer {
|
||||
private isReceivingStream:boolean = false;
|
||||
public toClose: 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({
|
||||
initiator: initiator ? initiator : false,
|
||||
reconnectTimer: 10000,
|
||||
@ -26,15 +28,17 @@ export class ScreenSharingPeer extends Peer {
|
||||
{
|
||||
urls: STUN_SERVER.split(',')
|
||||
},
|
||||
{
|
||||
TURN_SERVER !== '' ? {
|
||||
urls: TURN_SERVER.split(','),
|
||||
username: TURN_USER,
|
||||
credential: TURN_PASSWORD
|
||||
},
|
||||
]
|
||||
username: user.webRtcUser || TURN_USER,
|
||||
credential: user.webRtcPassword || TURN_PASSWORD
|
||||
} : undefined,
|
||||
].filter((value) => value !== undefined)
|
||||
}
|
||||
});
|
||||
|
||||
this.userId = user.userId;
|
||||
|
||||
//start listen signal for the peer connection
|
||||
this.on('signal', (data: unknown) => {
|
||||
this.sendWebrtcScreenSharingSignal(data);
|
||||
|
@ -9,15 +9,18 @@ import {
|
||||
UpdatedLocalStreamCallback
|
||||
} from "./MediaManager";
|
||||
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 {connectionManager} from "../Connexion/ConnectionManager";
|
||||
import {GameConnexionTypes} from "../Url/UrlManager";
|
||||
import {blackListManager} from "./BlackListManager";
|
||||
|
||||
export interface UserSimplePeerInterface{
|
||||
userId: number;
|
||||
name?: string;
|
||||
initiator?: boolean;
|
||||
webRtcUser?: string|undefined;
|
||||
webRtcPassword?: string|undefined;
|
||||
}
|
||||
|
||||
export interface PeerConnectionListener {
|
||||
@ -38,6 +41,7 @@ export class SimplePeer {
|
||||
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
|
||||
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
|
||||
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
|
||||
private readonly userId: number;
|
||||
|
||||
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.
|
||||
@ -48,6 +52,7 @@ export class SimplePeer {
|
||||
mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback);
|
||||
mediaManager.onStartScreenSharing(this.sendLocalScreenSharingStreamCallback);
|
||||
mediaManager.onStopScreenSharing(this.stopLocalScreenSharingStreamCallback);
|
||||
this.userId = Connection.getUserId();
|
||||
this.initialise();
|
||||
}
|
||||
|
||||
@ -91,15 +96,14 @@ export class SimplePeer {
|
||||
});
|
||||
}
|
||||
|
||||
private receiveWebrtcStart(user: UserSimplePeerInterface) {
|
||||
//this.WebRtcRoomId = data.roomId;
|
||||
private receiveWebrtcStart(user: UserSimplePeerInterface): void {
|
||||
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)
|
||||
// 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.
|
||||
|
||||
|
||||
//start connection
|
||||
console.log('receiveWebrtcStart. Initiator: ', user.initiator)
|
||||
//console.log('receiveWebrtcStart. Initiator: ', user.initiator)
|
||||
if(!user.initiator){
|
||||
return;
|
||||
}
|
||||
@ -136,13 +140,13 @@ export class SimplePeer {
|
||||
|
||||
mediaManager.removeActiveVideo("" + user.userId);
|
||||
|
||||
mediaManager.addActiveVideo("" + user.userId, name, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
|
||||
mediaManager.addActiveVideo(user, name);
|
||||
|
||||
const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
|
||||
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection);
|
||||
|
||||
//permit to send message
|
||||
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;
|
||||
@ -187,7 +191,7 @@ export class SimplePeer {
|
||||
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);
|
||||
|
||||
for (const peerConnectionListener of this.peerConnectionListeners) {
|
||||
@ -298,6 +302,7 @@ export class SimplePeer {
|
||||
}
|
||||
|
||||
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
|
||||
if (blackListManager.isBlackListed(data.userId)) return;
|
||||
console.log("receiveWebrtcScreenSharingSignal", data);
|
||||
try {
|
||||
//if offer type, create peer connection
|
||||
@ -390,6 +395,7 @@ export class SimplePeer {
|
||||
}
|
||||
|
||||
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 (this.PeerScreenSharingConnectionArray.has(userId)) {
|
||||
this.pushScreenSharingToRemoteUser(userId);
|
||||
|
@ -2,19 +2,30 @@ import * as SimplePeerNamespace from "simple-peer";
|
||||
import {mediaManager} from "./MediaManager";
|
||||
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
|
||||
import {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import {blackListManager} from "./BlackListManager";
|
||||
import {Subscription} from "rxjs";
|
||||
import {UserSimplePeerInterface} from "./SimplePeer";
|
||||
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||
|
||||
export const MESSAGE_TYPE_CONSTRAINT = 'constraint';
|
||||
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.
|
||||
*/
|
||||
export class VideoPeer extends Peer {
|
||||
public toClose: 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({
|
||||
initiator: initiator ? initiator : false,
|
||||
reconnectTimer: 10000,
|
||||
@ -23,43 +34,24 @@ export class VideoPeer extends Peer {
|
||||
{
|
||||
urls: STUN_SERVER.split(',')
|
||||
},
|
||||
{
|
||||
TURN_SERVER !== '' ? {
|
||||
urls: TURN_SERVER.split(','),
|
||||
username: TURN_USER,
|
||||
credential: TURN_PASSWORD
|
||||
},
|
||||
]
|
||||
username: user.webRtcUser || TURN_USER,
|
||||
credential: user.webRtcPassword || TURN_PASSWORD
|
||||
} : undefined,
|
||||
].filter((value) => value !== undefined)
|
||||
}
|
||||
});
|
||||
|
||||
console.log('PEER SETUP ', {
|
||||
initiator: initiator ? initiator : false,
|
||||
reconnectTimer: 10000,
|
||||
config: {
|
||||
iceServers: [
|
||||
{
|
||||
urls: STUN_SERVER.split(',')
|
||||
},
|
||||
{
|
||||
urls: TURN_SERVER.split(','),
|
||||
username: TURN_USER,
|
||||
credential: TURN_PASSWORD
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
this.userId = user.userId;
|
||||
this.userName = user.name || '';
|
||||
|
||||
//start listen signal for the peer connection
|
||||
this.on('signal', (data: unknown) => {
|
||||
this.sendWebrtcSignal(data);
|
||||
});
|
||||
|
||||
this.on('stream', (stream: MediaStream) => {
|
||||
this.stream(stream);
|
||||
});
|
||||
|
||||
/*peer.on('track', (track: MediaStreamTrack, stream: MediaStream) => {
|
||||
});*/
|
||||
this.on('stream', (stream: MediaStream) => this.stream(stream));
|
||||
|
||||
this.on('close', () => {
|
||||
this._connected = false;
|
||||
@ -70,7 +62,7 @@ export class VideoPeer extends Peer {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.on('error', (err: any) => {
|
||||
console.error(`error => ${this.userId} => ${err.code}`, err);
|
||||
mediaManager.isError("" + userId);
|
||||
mediaManager.isError("" + this.userId);
|
||||
});
|
||||
|
||||
this.on('connect', () => {
|
||||
@ -81,8 +73,6 @@ export class VideoPeer extends Peer {
|
||||
|
||||
this.on('data', (chunk: Buffer) => {
|
||||
const message = JSON.parse(chunk.toString('utf8'));
|
||||
console.log("data", message);
|
||||
|
||||
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||
if (message.audio) {
|
||||
mediaManager.enabledMicrophoneByUserId(this.userId);
|
||||
@ -95,8 +85,19 @@ export class VideoPeer extends Peer {
|
||||
} else {
|
||||
mediaManager.disabledVideoByUserId(this.userId);
|
||||
}
|
||||
} else if(message.type === 'message') {
|
||||
mediaManager.addNewMessage(message.name, message.message);
|
||||
} else if(message.type === MESSAGE_TYPE_MESSAGE) {
|
||||
if (!blackListManager.isBlackListed(message.userId)) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -105,6 +106,31 @@ export class VideoPeer extends Peer {
|
||||
});
|
||||
|
||||
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) {
|
||||
@ -120,13 +146,13 @@ export class VideoPeer extends Peer {
|
||||
*/
|
||||
private stream(stream: MediaStream) {
|
||||
try {
|
||||
this.remoteStream = stream;
|
||||
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
|
||||
this.toggleRemoteStream(false);
|
||||
}
|
||||
mediaManager.addStreamRemoteVideo("" + this.userId, stream);
|
||||
}catch (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){
|
||||
return;
|
||||
}
|
||||
this.onBlockSubscribe.unsubscribe();
|
||||
this.onUnBlockSubscribe.unsubscribe();
|
||||
mediaManager.removeActiveVideo("" + this.userId);
|
||||
// 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.
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -x
|
||||
set -o nounset errexit
|
||||
template_file_index=dist/index.html.tmpl
|
||||
template_file_index=dist/index.tmpl.html
|
||||
generated_file_index=dist/index.html
|
||||
tmp_trackcodefile=/tmp/trackcode
|
||||
|
||||
|
@ -39,7 +39,16 @@ module.exports = {
|
||||
plugins: [
|
||||
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({
|
||||
|
@ -4094,7 +4094,7 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
dependencies:
|
||||
aproba "^1.1.1"
|
||||
|
||||
rxjs@^6.6.0:
|
||||
rxjs@^6.6.0, rxjs@^6.6.3:
|
||||
version "6.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
|
||||
integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==
|
||||
|
4
maps/.dockerignore
Normal file
4
maps/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
/node_modules/
|
||||
/dist/bundle.js
|
||||
/yarn-error.log
|
||||
/Dockerfile
|
4
messages/.dockerignore
Normal file
4
messages/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
/node_modules/
|
||||
/dist/bundle.js
|
||||
/yarn-error.log
|
||||
/Dockerfile
|
@ -168,6 +168,8 @@ message WebRtcStartMessage {
|
||||
int32 userId = 1;
|
||||
string name = 2;
|
||||
bool initiator = 3;
|
||||
string webrtcUserName = 4;
|
||||
string webrtcPassword = 5;
|
||||
}
|
||||
|
||||
message WebRtcDisconnectMessage {
|
||||
@ -177,6 +179,8 @@ message WebRtcDisconnectMessage {
|
||||
message WebRtcSignalToClientMessage {
|
||||
int32 userId = 1;
|
||||
string signal = 2;
|
||||
string webrtcUserName = 4;
|
||||
string webrtcPassword = 5;
|
||||
}
|
||||
|
||||
message TeleportMessageMessage{
|
||||
|
@ -1,15 +1,26 @@
|
||||
FROM thecodingmachine/workadventure-back-base:latest as builder
|
||||
WORKDIR /var/www/messages
|
||||
COPY --chown=docker:docker messages .
|
||||
# protobuf build
|
||||
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
|
||||
WORKDIR /usr/src
|
||||
COPY messages .
|
||||
RUN yarn install && yarn proto
|
||||
|
||||
FROM thecodingmachine/nodejs:12
|
||||
|
||||
COPY --chown=docker:docker pusher .
|
||||
COPY --from=builder --chown=docker:docker /var/www/messages/generated /usr/src/app/src/Messages/generated
|
||||
# typescript build
|
||||
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
|
||||
WORKDIR /usr/src
|
||||
COPY pusher/yarn.lock pusher/package.json ./
|
||||
RUN yarn install
|
||||
|
||||
COPY pusher .
|
||||
COPY --from=builder /usr/src/generated src/Messages/generated
|
||||
ENV NODE_ENV=production
|
||||
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"]
|
||||
|
17
pusher/Dockerfile.prod
Normal file
17
pusher/Dockerfile.prod
Normal 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"]
|
@ -1,9 +1,19 @@
|
||||
FROM thecodingmachine/nodejs:12
|
||||
|
||||
COPY --chown=docker:docker uploader .
|
||||
# typescript build
|
||||
FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder2
|
||||
WORKDIR /usr/src
|
||||
COPY uploader/yarn.lock uploader/package.json ./
|
||||
RUN yarn install
|
||||
|
||||
COPY uploader .
|
||||
ENV NODE_ENV=production
|
||||
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"]
|
||||
|
5
website/.dockerignore
Normal file
5
website/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
/dist/
|
||||
/node_modules/
|
||||
/dist/bundle.js
|
||||
/yarn-error.log
|
||||
/Dockerfile
|
Loading…
Reference in New Issue
Block a user