Merge branch 'develop' of github.com:thecodingmachine/workadventure into develop
This commit is contained in:
commit
116c56be81
13
.github/workflows/continuous_integration.yml
vendored
13
.github/workflows/continuous_integration.yml
vendored
@ -64,6 +64,11 @@ jobs:
|
|||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
|
# We will enable prettier checks on front in a few month, when most PRs without prettier have been merged
|
||||||
|
# - name: "Prettier"
|
||||||
|
# run: yarn run pretty-check
|
||||||
|
# working-directory: "front"
|
||||||
|
|
||||||
continuous-integration-pusher:
|
continuous-integration-pusher:
|
||||||
name: "Continuous Integration Pusher"
|
name: "Continuous Integration Pusher"
|
||||||
|
|
||||||
@ -107,6 +112,10 @@ jobs:
|
|||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "pusher"
|
working-directory: "pusher"
|
||||||
|
|
||||||
|
- name: "Prettier"
|
||||||
|
run: yarn run pretty-check
|
||||||
|
working-directory: "pusher"
|
||||||
|
|
||||||
continuous-integration-back:
|
continuous-integration-back:
|
||||||
name: "Continuous Integration Back"
|
name: "Continuous Integration Back"
|
||||||
|
|
||||||
@ -150,3 +159,7 @@ jobs:
|
|||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
|
||||||
|
- name: "Prettier"
|
||||||
|
run: yarn run pretty-check
|
||||||
|
working-directory: "back"
|
||||||
|
|
||||||
|
@ -46,6 +46,15 @@ $ yarn run install
|
|||||||
$ yarn run prepare
|
$ yarn run prepare
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need
|
||||||
|
to run code linting manually:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ docker-compose exec front yarn run pretty
|
||||||
|
$ docker-compose exec pusher yarn run pretty
|
||||||
|
$ docker-compose exec back yarn run pretty
|
||||||
|
```
|
||||||
|
|
||||||
### Providing tests
|
### Providing tests
|
||||||
|
|
||||||
WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test.
|
WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test.
|
||||||
|
@ -10,8 +10,8 @@
|
|||||||
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
||||||
"profile": "tsc && node --prof ./dist/server.js",
|
"profile": "tsc && node --prof ./dist/server.js",
|
||||||
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||||
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
"lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts",
|
||||||
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
|
"fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
||||||
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// lib/app.ts
|
// lib/app.ts
|
||||||
import {PrometheusController} from "./Controller/PrometheusController";
|
import { PrometheusController } from "./Controller/PrometheusController";
|
||||||
import {DebugController} from "./Controller/DebugController";
|
import { DebugController } from "./Controller/DebugController";
|
||||||
import {App as uwsApp} from "./Server/sifrr.server";
|
import { App as uwsApp } from "./Server/sifrr.server";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public app: uwsApp;
|
public app: uwsApp;
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import {HttpResponse} from "uWebSockets.js";
|
import { HttpResponse } from "uWebSockets.js";
|
||||||
|
|
||||||
|
|
||||||
export class BaseController {
|
export class BaseController {
|
||||||
protected addCorsHeaders(res: HttpResponse): void {
|
protected addCorsHeaders(res: HttpResponse): void {
|
||||||
res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
|
||||||
res.writeHeader('access-control-allow-origin', '*');
|
res.writeHeader("access-control-allow-origin", "*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,54 @@
|
|||||||
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||||
import {stringify} from "circular-json";
|
import { stringify } from "circular-json";
|
||||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||||
import { parse } from 'query-string';
|
import { parse } from "query-string";
|
||||||
import {App} from "../Server/sifrr.server";
|
import { App } from "../Server/sifrr.server";
|
||||||
import {socketManager} from "../Services/SocketManager";
|
import { socketManager } from "../Services/SocketManager";
|
||||||
|
|
||||||
export class DebugController {
|
export class DebugController {
|
||||||
constructor(private App : App) {
|
constructor(private App: App) {
|
||||||
this.getDump();
|
this.getDump();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDump() {
|
||||||
getDump(){
|
|
||||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
if (query.token !== ADMIN_API_TOKEN) {
|
if (query.token !== ADMIN_API_TOKEN) {
|
||||||
return res.status(401).send('Invalid token sent!');
|
return res.status(401).send("Invalid token sent!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify(
|
return res
|
||||||
socketManager.getWorlds(),
|
.writeStatus("200 OK")
|
||||||
(key: unknown, value: unknown) => {
|
.writeHeader("Content-Type", "application/json")
|
||||||
if (key === 'listeners') {
|
.end(
|
||||||
return 'Listeners';
|
stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
|
||||||
}
|
if (key === "listeners") {
|
||||||
if (key === 'socket') {
|
return "Listeners";
|
||||||
return 'Socket';
|
|
||||||
}
|
|
||||||
if (key === 'batchedMessages') {
|
|
||||||
return 'BatchedMessages';
|
|
||||||
}
|
|
||||||
if(value instanceof Map) {
|
|
||||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
for (const [mapKey, mapValue] of value.entries()) {
|
|
||||||
obj[mapKey] = mapValue;
|
|
||||||
}
|
}
|
||||||
return obj;
|
if (key === "socket") {
|
||||||
} else if(value instanceof Set) {
|
return "Socket";
|
||||||
|
}
|
||||||
|
if (key === "batchedMessages") {
|
||||||
|
return "BatchedMessages";
|
||||||
|
}
|
||||||
|
if (value instanceof Map) {
|
||||||
|
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
for (const [mapKey, mapValue] of value.entries()) {
|
||||||
|
obj[mapKey] = mapValue;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
} else if (value instanceof Set) {
|
||||||
const obj: Array<unknown> = [];
|
const obj: Array<unknown> = [];
|
||||||
for (const [setKey, setValue] of value.entries()) {
|
for (const [setKey, setValue] of value.entries()) {
|
||||||
obj.push(setValue);
|
obj.push(setValue);
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
));
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {App} from "../Server/sifrr.server";
|
import { App } from "../Server/sifrr.server";
|
||||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||||
const register = require('prom-client').register;
|
const register = require("prom-client").register;
|
||||||
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
|
const collectDefaultMetrics = require("prom-client").collectDefaultMetrics;
|
||||||
|
|
||||||
export class PrometheusController {
|
export class PrometheusController {
|
||||||
constructor(private App: App) {
|
constructor(private App: App) {
|
||||||
@ -14,7 +14,7 @@ export class PrometheusController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private metrics(res: HttpResponse, req: HttpRequest): void {
|
private metrics(res: HttpResponse, req: HttpRequest): void {
|
||||||
res.writeHeader('Content-Type', register.contentType);
|
res.writeHeader("Content-Type", register.contentType);
|
||||||
res.end(register.metrics());
|
res.end(register.metrics());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
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;
|
||||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || '';
|
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
|
||||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
|
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
|
||||||
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
||||||
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
|
||||||
const JITSI_ISS = process.env.JITSI_ISS || '';
|
const JITSI_ISS = process.env.JITSI_ISS || "";
|
||||||
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
|
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 const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
||||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4');
|
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
@ -24,5 +24,5 @@ export {
|
|||||||
CPU_OVERHEAT_THRESHOLD,
|
CPU_OVERHEAT_THRESHOLD,
|
||||||
JITSI_URL,
|
JITSI_URL,
|
||||||
JITSI_ISS,
|
JITSI_ISS,
|
||||||
SECRET_JITSI_KEY
|
SECRET_JITSI_KEY,
|
||||||
}
|
};
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
UserJoinedRoomMessage, UserLeftRoomMessage
|
UserJoinedRoomMessage,
|
||||||
|
UserLeftRoomMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import {AdminSocket} from "../RoomManager";
|
import { AdminSocket } from "../RoomManager";
|
||||||
|
|
||||||
|
|
||||||
export class Admin {
|
export class Admin {
|
||||||
public constructor(
|
public constructor(private readonly socket: AdminSocket) {}
|
||||||
private readonly socket: AdminSocket
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendUserJoin(uuid: string, name: string, ip: string): void {
|
public sendUserJoin(uuid: string, name: string, ip: string): void {
|
||||||
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
||||||
@ -24,7 +21,7 @@ export class Admin {
|
|||||||
this.socket.write(serverToAdminClientMessage);
|
this.socket.write(serverToAdminClientMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendUserLeft(uuid: string/*, name: string, ip: string*/): void {
|
public sendUserLeft(uuid: string /*, name: string, ip: string*/): void {
|
||||||
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
||||||
|
|
||||||
const userLeftRoomMessage = new UserLeftRoomMessage();
|
const userLeftRoomMessage = new UserLeftRoomMessage();
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import {PointInterface} from "./Websocket/PointInterface";
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
import {Group} from "./Group";
|
import { Group } from "./Group";
|
||||||
import {User, UserSocket} from "./User";
|
import { User, UserSocket } from "./User";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
|
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||||
import {PositionNotifier} from "./PositionNotifier";
|
import { PositionNotifier } from "./PositionNotifier";
|
||||||
import {Movable} from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
|
import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
|
||||||
import {arrayIntersect} from "../Services/ArrayHelper";
|
import { arrayIntersect } from "../Services/ArrayHelper";
|
||||||
import {EmoteEventMessage, JoinRoomMessage} from "../Messages/generated/messages_pb";
|
import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb";
|
||||||
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import {ZoneSocket} from "src/RoomManager";
|
import { ZoneSocket } from "src/RoomManager";
|
||||||
import {Admin} from "../Model/Admin";
|
import { Admin } from "../Model/Admin";
|
||||||
|
|
||||||
export type ConnectCallback = (user: User, group: Group) => void;
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
@ -39,33 +39,33 @@ export class GameRoom {
|
|||||||
private readonly positionNotifier: PositionNotifier;
|
private readonly positionNotifier: PositionNotifier;
|
||||||
public readonly roomId: string;
|
public readonly roomId: string;
|
||||||
public readonly roomSlug: string;
|
public readonly roomSlug: string;
|
||||||
public readonly worldSlug: string = '';
|
public readonly worldSlug: string = "";
|
||||||
public readonly organizationSlug: string = '';
|
public readonly organizationSlug: string = "";
|
||||||
private versionNumber:number = 1;
|
private versionNumber: number = 1;
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
|
|
||||||
constructor(roomId: string,
|
constructor(
|
||||||
connectCallback: ConnectCallback,
|
roomId: string,
|
||||||
disconnectCallback: DisconnectCallback,
|
connectCallback: ConnectCallback,
|
||||||
minDistance: number,
|
disconnectCallback: DisconnectCallback,
|
||||||
groupRadius: number,
|
minDistance: number,
|
||||||
onEnters: EntersCallback,
|
groupRadius: number,
|
||||||
onMoves: MovesCallback,
|
onEnters: EntersCallback,
|
||||||
onLeaves: LeavesCallback,
|
onMoves: MovesCallback,
|
||||||
onEmote: EmoteCallback,
|
onLeaves: LeavesCallback,
|
||||||
|
onEmote: EmoteCallback
|
||||||
) {
|
) {
|
||||||
this.roomId = roomId;
|
this.roomId = roomId;
|
||||||
|
|
||||||
if (isRoomAnonymous(roomId)) {
|
if (isRoomAnonymous(roomId)) {
|
||||||
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
||||||
} else {
|
} else {
|
||||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
|
const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
|
||||||
this.roomSlug = roomSlug;
|
this.roomSlug = roomSlug;
|
||||||
this.organizationSlug = organizationSlug;
|
this.organizationSlug = organizationSlug;
|
||||||
this.worldSlug = worldSlug;
|
this.worldSlug = worldSlug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.users = new Map<number, User>();
|
this.users = new Map<number, User>();
|
||||||
this.usersByUuid = new Map<string, User>();
|
this.usersByUuid = new Map<string, User>();
|
||||||
this.admins = new Set<Admin>();
|
this.admins = new Set<Admin>();
|
||||||
@ -86,21 +86,22 @@ export class GameRoom {
|
|||||||
return this.users;
|
return this.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserByUuid(uuid: string): User|undefined {
|
public getUserByUuid(uuid: string): User | undefined {
|
||||||
return this.usersByUuid.get(uuid);
|
return this.usersByUuid.get(uuid);
|
||||||
}
|
}
|
||||||
public getUserById(id: number): User|undefined {
|
public getUserById(id: number): User | undefined {
|
||||||
return this.users.get(id);
|
return this.users.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User {
|
public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User {
|
||||||
const positionMessage = joinRoomMessage.getPositionmessage();
|
const positionMessage = joinRoomMessage.getPositionmessage();
|
||||||
if (positionMessage === undefined) {
|
if (positionMessage === undefined) {
|
||||||
throw new Error('Missing position message');
|
throw new Error("Missing position message");
|
||||||
}
|
}
|
||||||
const position = ProtobufUtils.toPointInterface(positionMessage);
|
const position = ProtobufUtils.toPointInterface(positionMessage);
|
||||||
|
|
||||||
const user = new User(this.nextUserId,
|
const user = new User(
|
||||||
|
this.nextUserId,
|
||||||
joinRoomMessage.getUseruuid(),
|
joinRoomMessage.getUseruuid(),
|
||||||
joinRoomMessage.getIpaddress(),
|
joinRoomMessage.getIpaddress(),
|
||||||
position,
|
position,
|
||||||
@ -126,12 +127,12 @@ export class GameRoom {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public leave(user : User){
|
public leave(user: User) {
|
||||||
const userObj = this.users.get(user.id);
|
const userObj = this.users.get(user.id);
|
||||||
if (userObj === undefined) {
|
if (userObj === undefined) {
|
||||||
console.warn('User ', user.id, 'does not belong to this game room! It should!');
|
console.warn("User ", user.id, "does not belong to this game room! It should!");
|
||||||
}
|
}
|
||||||
if (userObj !== undefined && typeof userObj.group !== 'undefined') {
|
if (userObj !== undefined && typeof userObj.group !== "undefined") {
|
||||||
this.leaveGroup(userObj);
|
this.leaveGroup(userObj);
|
||||||
}
|
}
|
||||||
this.users.delete(user.id);
|
this.users.delete(user.id);
|
||||||
@ -143,7 +144,7 @@ export class GameRoom {
|
|||||||
|
|
||||||
// Notify admins
|
// Notify admins
|
||||||
for (const admin of this.admins) {
|
for (const admin of this.admins) {
|
||||||
admin.sendUserLeft(user.uuid/*, user.name, user.IPAddress*/);
|
admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +152,7 @@ export class GameRoom {
|
|||||||
return this.users.size === 0 && this.admins.size === 0;
|
return this.users.size === 0 && this.admins.size === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePosition(user : User, userPosition: PointInterface): void {
|
public updatePosition(user: User, userPosition: PointInterface): void {
|
||||||
user.setPosition(userPosition);
|
user.setPosition(userPosition);
|
||||||
|
|
||||||
this.updateUserGroup(user);
|
this.updateUserGroup(user);
|
||||||
@ -173,22 +174,24 @@ export class GameRoom {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user);
|
const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user);
|
||||||
|
|
||||||
if (closestItem !== null) {
|
if (closestItem !== null) {
|
||||||
if (closestItem instanceof Group) {
|
if (closestItem instanceof Group) {
|
||||||
// Let's join the group!
|
// Let's join the group!
|
||||||
closestItem.join(user);
|
closestItem.join(user);
|
||||||
} else {
|
} else {
|
||||||
const closestUser : User = closestItem;
|
const closestUser: User = closestItem;
|
||||||
const group: Group = new Group(this.roomId,[
|
const group: Group = new Group(
|
||||||
user,
|
this.roomId,
|
||||||
closestUser
|
[user, closestUser],
|
||||||
], this.connectCallback, this.disconnectCallback, this.positionNotifier);
|
this.connectCallback,
|
||||||
|
this.disconnectCallback,
|
||||||
|
this.positionNotifier
|
||||||
|
);
|
||||||
this.groups.add(group);
|
this.groups.add(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// If the user is part of a group:
|
// If the user is part of a group:
|
||||||
// should he leave the group?
|
// should he leave the group?
|
||||||
@ -229,7 +232,9 @@ export class GameRoom {
|
|||||||
this.positionNotifier.leave(group);
|
this.positionNotifier.leave(group);
|
||||||
group.destroy();
|
group.destroy();
|
||||||
if (!this.groups.has(group)) {
|
if (!this.groups.has(group)) {
|
||||||
throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World.");
|
throw new Error(
|
||||||
|
"Could not find group " + group.getId() + " referenced by user " + user.id + " in World."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.groups.delete(group);
|
this.groups.delete(group);
|
||||||
//todo: is the group garbage collected?
|
//todo: is the group garbage collected?
|
||||||
@ -247,16 +252,15 @@ export class GameRoom {
|
|||||||
* OR
|
* OR
|
||||||
* - close enough to a group (distance <= groupRadius)
|
* - close enough to a group (distance <= groupRadius)
|
||||||
*/
|
*/
|
||||||
private searchClosestAvailableUserOrGroup(user: User): User|Group|null
|
private searchClosestAvailableUserOrGroup(user: User): User | Group | null {
|
||||||
{
|
|
||||||
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
||||||
let matchingItem: User | Group | null = null;
|
let matchingItem: User | Group | null = null;
|
||||||
this.users.forEach((currentUser, userId) => {
|
this.users.forEach((currentUser, userId) => {
|
||||||
// Let's only check users that are not part of a group
|
// Let's only check users that are not part of a group
|
||||||
if (typeof currentUser.group !== 'undefined') {
|
if (typeof currentUser.group !== "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(currentUser === user) {
|
if (currentUser === user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentUser.silent) {
|
if (currentUser.silent) {
|
||||||
@ -265,7 +269,7 @@ export class GameRoom {
|
|||||||
|
|
||||||
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
||||||
|
|
||||||
if(distance <= minimumDistanceFound && distance <= this.minDistance) {
|
if (distance <= minimumDistanceFound && distance <= this.minDistance) {
|
||||||
minimumDistanceFound = distance;
|
minimumDistanceFound = distance;
|
||||||
matchingItem = currentUser;
|
matchingItem = currentUser;
|
||||||
}
|
}
|
||||||
@ -276,7 +280,7 @@ export class GameRoom {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
||||||
if(distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
if (distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
||||||
minimumDistanceFound = distance;
|
minimumDistanceFound = distance;
|
||||||
matchingItem = group;
|
matchingItem = group;
|
||||||
}
|
}
|
||||||
@ -285,15 +289,15 @@ export class GameRoom {
|
|||||||
return matchingItem;
|
return matchingItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static computeDistance(user1: User, user2: User): number
|
public static computeDistance(user1: User, user2: User): number {
|
||||||
{
|
|
||||||
const user1Position = user1.getPosition();
|
const user1Position = user1.getPosition();
|
||||||
const user2Position = user2.getPosition();
|
const user2Position = user2.getPosition();
|
||||||
return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2));
|
return Math.sqrt(
|
||||||
|
Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number
|
public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number {
|
||||||
{
|
|
||||||
return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2));
|
return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,9 +329,9 @@ export class GameRoom {
|
|||||||
public adminLeave(admin: Admin): void {
|
public adminLeave(admin: Admin): void {
|
||||||
this.admins.delete(admin);
|
this.admins.delete(admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public incrementVersion(): number {
|
public incrementVersion(): number {
|
||||||
this.versionNumber++
|
this.versionNumber++;
|
||||||
return this.versionNumber;
|
return this.versionNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { ConnectCallback, DisconnectCallback } from "./GameRoom";
|
import { ConnectCallback, DisconnectCallback } from "./GameRoom";
|
||||||
import { User } from "./User";
|
import { User } from "./User";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
import {Movable} from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import {PositionNotifier} from "_Model/PositionNotifier";
|
import { PositionNotifier } from "_Model/PositionNotifier";
|
||||||
import {gaugeManager} from "../Services/GaugeManager";
|
import { gaugeManager } from "../Services/GaugeManager";
|
||||||
import {MAX_PER_GROUP} from "../Enum/EnvironmentVariable";
|
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
export class Group implements Movable {
|
export class Group implements Movable {
|
||||||
|
|
||||||
private static nextId: number = 1;
|
private static nextId: number = 1;
|
||||||
|
|
||||||
private id: number;
|
private id: number;
|
||||||
@ -18,8 +17,13 @@ export class Group implements Movable {
|
|||||||
private wasDestroyed: boolean = false;
|
private wasDestroyed: boolean = false;
|
||||||
private roomId: string;
|
private roomId: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) {
|
roomId: string,
|
||||||
|
users: User[],
|
||||||
|
private connectCallback: ConnectCallback,
|
||||||
|
private disconnectCallback: DisconnectCallback,
|
||||||
|
private positionNotifier: PositionNotifier
|
||||||
|
) {
|
||||||
this.roomId = roomId;
|
this.roomId = roomId;
|
||||||
this.users = new Set<User>();
|
this.users = new Set<User>();
|
||||||
this.id = Group.nextId;
|
this.id = Group.nextId;
|
||||||
@ -43,7 +47,7 @@ export class Group implements Movable {
|
|||||||
return Array.from(this.users.values());
|
return Array.from(this.users.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() : number {
|
getId(): number {
|
||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +57,7 @@ export class Group implements Movable {
|
|||||||
getPosition(): PositionInterface {
|
getPosition(): PositionInterface {
|
||||||
return {
|
return {
|
||||||
x: this.x,
|
x: this.x,
|
||||||
y: this.y
|
y: this.y,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +87,7 @@ export class Group implements Movable {
|
|||||||
if (oldX === undefined) {
|
if (oldX === undefined) {
|
||||||
this.positionNotifier.enter(this);
|
this.positionNotifier.enter(this);
|
||||||
} else {
|
} else {
|
||||||
this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY});
|
this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,19 +99,17 @@ export class Group implements Movable {
|
|||||||
return this.users.size <= 1;
|
return this.users.size <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
join(user: User): void
|
join(user: User): void {
|
||||||
{
|
|
||||||
// Broadcast on the right event
|
// Broadcast on the right event
|
||||||
this.connectCallback(user, this);
|
this.connectCallback(user, this);
|
||||||
this.users.add(user);
|
this.users.add(user);
|
||||||
user.group = this;
|
user.group = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
leave(user: User): void
|
leave(user: User): void {
|
||||||
{
|
|
||||||
const success = this.users.delete(user);
|
const success = this.users.delete(user);
|
||||||
if (success === false) {
|
if (success === false) {
|
||||||
throw new Error("Could not find user "+user.id+" in the group "+this.id);
|
throw new Error("Could not find user " + user.id + " in the group " + this.id);
|
||||||
}
|
}
|
||||||
user.group = undefined;
|
user.group = undefined;
|
||||||
|
|
||||||
@ -123,8 +125,7 @@ export class Group implements Movable {
|
|||||||
* Let's kick everybody out.
|
* Let's kick everybody out.
|
||||||
* Usually used when there is only one user left.
|
* Usually used when there is only one user left.
|
||||||
*/
|
*/
|
||||||
destroy(): void
|
destroy(): void {
|
||||||
{
|
|
||||||
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
|
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
|
||||||
for (const user of this.users) {
|
for (const user of this.users) {
|
||||||
this.leave(user);
|
this.leave(user);
|
||||||
@ -132,7 +133,7 @@ export class Group implements Movable {
|
|||||||
this.wasDestroyed = true;
|
this.wasDestroyed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get getSize(){
|
get getSize() {
|
||||||
return this.users.size;
|
return this.users.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A physical object that can be placed into a Zone
|
* A physical object that can be placed into a Zone
|
||||||
*/
|
*/
|
||||||
export interface Movable {
|
export interface Movable {
|
||||||
getPosition(): PositionInterface
|
getPosition(): PositionInterface;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface PositionInterface {
|
export interface PositionInterface {
|
||||||
x: number,
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,12 @@
|
|||||||
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
||||||
* number of players around the current player.
|
* number of players around the current player.
|
||||||
*/
|
*/
|
||||||
import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
|
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone";
|
||||||
import {Movable} from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
import {ZoneSocket} from "../RoomManager";
|
import { ZoneSocket } from "../RoomManager";
|
||||||
import {User} from "_Model/User";
|
import { User } from "_Model/User";
|
||||||
import {EmoteEventMessage} from "../Messages/generated/messages_pb";
|
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
interface ZoneDescriptor {
|
interface ZoneDescriptor {
|
||||||
i: number;
|
i: number;
|
||||||
@ -21,19 +21,24 @@ interface ZoneDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PositionNotifier {
|
export class PositionNotifier {
|
||||||
|
|
||||||
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
||||||
|
|
||||||
private zones: Zone[][] = [];
|
private zones: Zone[][] = [];
|
||||||
|
|
||||||
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback, private onEmote: EmoteCallback) {
|
constructor(
|
||||||
}
|
private zoneWidth: number,
|
||||||
|
private zoneHeight: number,
|
||||||
|
private onUserEnters: EntersCallback,
|
||||||
|
private onUserMoves: MovesCallback,
|
||||||
|
private onUserLeaves: LeavesCallback,
|
||||||
|
private onEmote: EmoteCallback
|
||||||
|
) {}
|
||||||
|
|
||||||
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||||
return {
|
return {
|
||||||
i: Math.floor(x / this.zoneWidth),
|
i: Math.floor(x / this.zoneWidth),
|
||||||
j: Math.floor(y / this.zoneHeight),
|
j: Math.floor(y / this.zoneHeight),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public enter(thing: Movable): void {
|
public enter(thing: Movable): void {
|
||||||
@ -100,6 +105,5 @@ export class PositionNotifier {
|
|||||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y);
|
const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y);
|
||||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
||||||
zone.emitEmoteEvent(emoteEventMessage);
|
zone.emitEmoteEvent(emoteEventMessage);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
//helper functions to parse room IDs
|
//helper functions to parse room IDs
|
||||||
|
|
||||||
export const isRoomAnonymous = (roomID: string): boolean => {
|
export const isRoomAnonymous = (roomID: string): boolean => {
|
||||||
if (roomID.startsWith('_/')) {
|
if (roomID.startsWith("_/")) {
|
||||||
return true;
|
return true;
|
||||||
} else if(roomID.startsWith('@/')) {
|
} else if (roomID.startsWith("@/")) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Incorrect room ID: '+roomID);
|
throw new Error("Incorrect room ID: " + roomID);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
||||||
const idParts = roomId.split('/');
|
const idParts = roomId.split("/");
|
||||||
if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId);
|
if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId);
|
||||||
return idParts.slice(2).join('/');
|
return idParts.slice(2).join("/");
|
||||||
}
|
};
|
||||||
export interface extractDataFromPrivateRoomIdResponse {
|
export interface extractDataFromPrivateRoomIdResponse {
|
||||||
organizationSlug: string;
|
organizationSlug: string;
|
||||||
worldSlug: string;
|
worldSlug: string;
|
||||||
roomSlug: string;
|
roomSlug: string;
|
||||||
}
|
}
|
||||||
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
||||||
const idParts = roomId.split('/');
|
const idParts = roomId.split("/");
|
||||||
if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId);
|
if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId);
|
||||||
const organizationSlug = idParts[1];
|
const organizationSlug = idParts[1];
|
||||||
const worldSlug = idParts[2];
|
const worldSlug = idParts[2];
|
||||||
const roomSlug = idParts[3];
|
const roomSlug = idParts[3];
|
||||||
return {organizationSlug, worldSlug, roomSlug}
|
return { organizationSlug, worldSlug, roomSlug };
|
||||||
}
|
};
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import { Group } from "./Group";
|
import { Group } from "./Group";
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
import {Zone} from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import {Movable} from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import {PositionNotifier} from "_Model/PositionNotifier";
|
import { PositionNotifier } from "_Model/PositionNotifier";
|
||||||
import {ServerDuplexStream} from "grpc";
|
import { ServerDuplexStream } from "grpc";
|
||||||
import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb";
|
import {
|
||||||
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
|
BatchMessage,
|
||||||
|
CompanionMessage,
|
||||||
|
PusherToBackMessage,
|
||||||
|
ServerToClientMessage,
|
||||||
|
SubMessage,
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
|
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
||||||
|
|
||||||
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
||||||
|
|
||||||
@ -22,7 +28,7 @@ export class User implements Movable {
|
|||||||
private positionNotifier: PositionNotifier,
|
private positionNotifier: PositionNotifier,
|
||||||
public readonly socket: UserSocket,
|
public readonly socket: UserSocket,
|
||||||
public readonly tags: string[],
|
public readonly tags: string[],
|
||||||
public readonly visitCardUrl: string|null,
|
public readonly visitCardUrl: string | null,
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly characterLayers: CharacterLayer[],
|
public readonly characterLayers: CharacterLayer[],
|
||||||
public readonly companion?: CompanionMessage
|
public readonly companion?: CompanionMessage
|
||||||
@ -42,9 +48,8 @@ export class User implements Movable {
|
|||||||
this.positionNotifier.updatePosition(this, position, oldPosition);
|
this.positionNotifier.updatePosition(this, position, oldPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private batchedMessages: BatchMessage = new BatchMessage();
|
private batchedMessages: BatchMessage = new BatchMessage();
|
||||||
private batchTimeout: NodeJS.Timeout|null = null;
|
private batchTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
public emitInBatch(payload: SubMessage): void {
|
public emitInBatch(payload: SubMessage): void {
|
||||||
this.batchedMessages.addPayload(payload);
|
this.batchedMessages.addPayload(payload);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface CharacterLayer {
|
export interface CharacterLayer {
|
||||||
name: string,
|
name: string;
|
||||||
url: string|undefined
|
url: string | undefined;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isItemEventMessageInterface =
|
export const isItemEventMessageInterface = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
itemId: tg.isNumber,
|
itemId: tg.isNumber,
|
||||||
event: tg.isString,
|
event: tg.isString,
|
||||||
state: tg.isUnknown,
|
state: tg.isUnknown,
|
||||||
parameters: tg.isUnknown,
|
parameters: tg.isUnknown,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import {PointInterface} from "./PointInterface";
|
import { PointInterface } from "./PointInterface";
|
||||||
|
|
||||||
export class Point implements PointInterface{
|
export class Point implements PointInterface {
|
||||||
constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) {
|
constructor(
|
||||||
}
|
public x: number,
|
||||||
|
public y: number,
|
||||||
|
public direction: string = "none",
|
||||||
|
public moving: boolean = false
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,11 +7,12 @@ import * as tg from "generic-type-guard";
|
|||||||
readonly moving: boolean;
|
readonly moving: boolean;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
export const isPointInterface =
|
export const isPointInterface = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
x: tg.isNumber,
|
x: tg.isNumber,
|
||||||
y: tg.isNumber,
|
y: tg.isNumber,
|
||||||
direction: tg.isString,
|
direction: tg.isString,
|
||||||
moving: tg.isBoolean
|
moving: tg.isBoolean,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
||||||
|
@ -1,34 +1,33 @@
|
|||||||
import {PointInterface} from "./PointInterface";
|
import { PointInterface } from "./PointInterface";
|
||||||
import {
|
import {
|
||||||
CharacterLayerMessage,
|
CharacterLayerMessage,
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
PointMessage,
|
PointMessage,
|
||||||
PositionMessage
|
PositionMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
|
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
||||||
import Direction = PositionMessage.Direction;
|
import Direction = PositionMessage.Direction;
|
||||||
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage";
|
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
|
|
||||||
export class ProtobufUtils {
|
export class ProtobufUtils {
|
||||||
|
|
||||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
public static toPositionMessage(point: PointInterface): PositionMessage {
|
||||||
let direction: Direction;
|
let direction: Direction;
|
||||||
switch (point.direction) {
|
switch (point.direction) {
|
||||||
case 'up':
|
case "up":
|
||||||
direction = Direction.UP;
|
direction = Direction.UP;
|
||||||
break;
|
break;
|
||||||
case 'down':
|
case "down":
|
||||||
direction = Direction.DOWN;
|
direction = Direction.DOWN;
|
||||||
break;
|
break;
|
||||||
case 'left':
|
case "left":
|
||||||
direction = Direction.LEFT;
|
direction = Direction.LEFT;
|
||||||
break;
|
break;
|
||||||
case 'right':
|
case "right":
|
||||||
direction = Direction.RIGHT;
|
direction = Direction.RIGHT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('unexpected direction');
|
throw new Error("unexpected direction");
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = new PositionMessage();
|
const position = new PositionMessage();
|
||||||
@ -44,16 +43,16 @@ export class ProtobufUtils {
|
|||||||
let direction: string;
|
let direction: string;
|
||||||
switch (position.getDirection()) {
|
switch (position.getDirection()) {
|
||||||
case Direction.UP:
|
case Direction.UP:
|
||||||
direction = 'up';
|
direction = "up";
|
||||||
break;
|
break;
|
||||||
case Direction.DOWN:
|
case Direction.DOWN:
|
||||||
direction = 'down';
|
direction = "down";
|
||||||
break;
|
break;
|
||||||
case Direction.LEFT:
|
case Direction.LEFT:
|
||||||
direction = 'left';
|
direction = "left";
|
||||||
break;
|
break;
|
||||||
case Direction.RIGHT:
|
case Direction.RIGHT:
|
||||||
direction = 'right';
|
direction = "right";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Unexpected direction");
|
throw new Error("Unexpected direction");
|
||||||
@ -82,7 +81,7 @@ export class ProtobufUtils {
|
|||||||
event: itemEventMessage.getEvent(),
|
event: itemEventMessage.getEvent(),
|
||||||
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
||||||
state: JSON.parse(itemEventMessage.getStatejson()),
|
state: JSON.parse(itemEventMessage.getStatejson()),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
||||||
@ -96,7 +95,7 @@ export class ProtobufUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
||||||
return characterLayers.map(function(characterLayer): CharacterLayerMessage {
|
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
||||||
const message = new CharacterLayerMessage();
|
const message = new CharacterLayerMessage();
|
||||||
message.setName(characterLayer.name);
|
message.setName(characterLayer.name);
|
||||||
if (characterLayer.url) {
|
if (characterLayer.url) {
|
||||||
@ -107,7 +106,7 @@ export class ProtobufUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
|
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
|
||||||
return characterLayers.map(function(characterLayer): CharacterLayer {
|
return characterLayers.map(function (characterLayer): CharacterLayer {
|
||||||
const url = characterLayer.getUrl();
|
const url = characterLayer.getUrl();
|
||||||
return {
|
return {
|
||||||
name: characterLayer.getName(),
|
name: characterLayer.getName(),
|
||||||
|
@ -1,35 +1,52 @@
|
|||||||
import {User} from "./User";
|
import { User } from "./User";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
import {Movable} from "./Movable";
|
import { Movable } from "./Movable";
|
||||||
import {Group} from "./Group";
|
import { Group } from "./Group";
|
||||||
import {ZoneSocket} from "../RoomManager";
|
import { ZoneSocket } from "../RoomManager";
|
||||||
import {EmoteEventMessage} from "../Messages/generated/messages_pb";
|
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void;
|
export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
|
||||||
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
|
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
|
||||||
export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void;
|
export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
|
||||||
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
|
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
|
||||||
|
|
||||||
export class Zone {
|
export class Zone {
|
||||||
private things: Set<Movable> = new Set<Movable>();
|
private things: Set<Movable> = new Set<Movable>();
|
||||||
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { }
|
private onEnters: EntersCallback,
|
||||||
|
private onMoves: MovesCallback,
|
||||||
|
private onLeaves: LeavesCallback,
|
||||||
|
private onEmote: EmoteCallback,
|
||||||
|
public readonly x: number,
|
||||||
|
public readonly y: number
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A user/thing leaves the zone
|
* A user/thing leaves the zone
|
||||||
*/
|
*/
|
||||||
public leave(thing: Movable, newZone: Zone|null) {
|
public leave(thing: Movable, newZone: Zone | null) {
|
||||||
const result = this.things.delete(thing);
|
const result = this.things.delete(thing);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
throw new Error('Could not find user in zone '+thing.id);
|
throw new Error("Could not find user in zone " + thing.id);
|
||||||
}
|
}
|
||||||
if (thing instanceof Group) {
|
if (thing instanceof Group) {
|
||||||
throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')');
|
throw new Error(
|
||||||
|
"Could not find group " +
|
||||||
|
thing.getId() +
|
||||||
|
" in zone (" +
|
||||||
|
this.x +
|
||||||
|
"," +
|
||||||
|
this.y +
|
||||||
|
"). Position of group: (" +
|
||||||
|
thing.getPosition().x +
|
||||||
|
"," +
|
||||||
|
thing.getPosition().y +
|
||||||
|
")"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
this.notifyLeft(thing, newZone);
|
this.notifyLeft(thing, newZone);
|
||||||
}
|
}
|
||||||
@ -37,13 +54,13 @@ export class Zone {
|
|||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this user/thing left
|
* Notify listeners of this zone that this user/thing left
|
||||||
*/
|
*/
|
||||||
private notifyLeft(thing: Movable, newZone: Zone|null) {
|
private notifyLeft(thing: Movable, newZone: Zone | null) {
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
this.onLeaves(thing, newZone, listener);
|
this.onLeaves(thing, newZone, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
|
public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
||||||
this.things.add(thing);
|
this.things.add(thing);
|
||||||
this.notifyEnter(thing, oldZone, position);
|
this.notifyEnter(thing, oldZone, position);
|
||||||
}
|
}
|
||||||
@ -51,13 +68,12 @@ export class Zone {
|
|||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this user entered
|
* Notify listeners of this zone that this user entered
|
||||||
*/
|
*/
|
||||||
private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
|
private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
this.onEnters(thing, oldZone, listener);
|
this.onEnters(thing, oldZone, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public move(thing: Movable, position: PositionInterface) {
|
public move(thing: Movable, position: PositionInterface) {
|
||||||
if (!this.things.has(thing)) {
|
if (!this.things.has(thing)) {
|
||||||
this.things.add(thing);
|
this.things.add(thing);
|
||||||
@ -67,7 +83,7 @@ export class Zone {
|
|||||||
|
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
//if (listener !== thing) {
|
//if (listener !== thing) {
|
||||||
this.onMoves(thing,position, listener);
|
this.onMoves(thing, position, listener);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,6 +105,5 @@ export class Zone {
|
|||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
this.onEmote(emoteEventMessage, listener);
|
this.onEmote(emoteEventMessage, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb";
|
import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb";
|
||||||
import {
|
import {
|
||||||
AdminGlobalMessage,
|
AdminGlobalMessage,
|
||||||
AdminMessage,
|
AdminMessage,
|
||||||
@ -11,92 +11,114 @@ import {
|
|||||||
JoinRoomMessage,
|
JoinRoomMessage,
|
||||||
PlayGlobalMessage,
|
PlayGlobalMessage,
|
||||||
PusherToBackMessage,
|
PusherToBackMessage,
|
||||||
QueryJitsiJwtMessage, RefreshRoomPromptMessage,
|
QueryJitsiJwtMessage,
|
||||||
|
RefreshRoomPromptMessage,
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
ServerToClientMessage,
|
ServerToClientMessage,
|
||||||
SilentMessage,
|
SilentMessage,
|
||||||
UserMovesMessage,
|
UserMovesMessage,
|
||||||
WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage,
|
WebRtcSignalToServerMessage,
|
||||||
ZoneMessage
|
WorldFullWarningToRoomMessage,
|
||||||
|
ZoneMessage,
|
||||||
} from "./Messages/generated/messages_pb";
|
} from "./Messages/generated/messages_pb";
|
||||||
import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc";
|
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
||||||
import {socketManager} from "./Services/SocketManager";
|
import { socketManager } from "./Services/SocketManager";
|
||||||
import {emitError} from "./Services/MessageHelpers";
|
import { emitError } from "./Services/MessageHelpers";
|
||||||
import {User, UserSocket} from "./Model/User";
|
import { User, UserSocket } from "./Model/User";
|
||||||
import {GameRoom} from "./Model/GameRoom";
|
import { GameRoom } from "./Model/GameRoom";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
import {Admin} from "./Model/Admin";
|
import { Admin } from "./Model/Admin";
|
||||||
|
|
||||||
const debug = Debug('roommanager');
|
const debug = Debug("roommanager");
|
||||||
|
|
||||||
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
|
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
|
||||||
|
|
||||||
const roomManager: IRoomManagerServer = {
|
const roomManager: IRoomManagerServer = {
|
||||||
joinRoom: (call: UserSocket): void => {
|
joinRoom: (call: UserSocket): void => {
|
||||||
console.log('joinRoom called');
|
console.log("joinRoom called");
|
||||||
|
|
||||||
let room: GameRoom|null = null;
|
let room: GameRoom | null = null;
|
||||||
let user: User|null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
call.on('data', (message: PusherToBackMessage) => {
|
call.on("data", (message: PusherToBackMessage) => {
|
||||||
try {
|
try {
|
||||||
if (room === null || user === null) {
|
if (room === null || user === null) {
|
||||||
if (message.hasJoinroommessage()) {
|
if (message.hasJoinroommessage()) {
|
||||||
socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => {
|
socketManager
|
||||||
if (call.writable) {
|
.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
|
||||||
room = gameRoom;
|
.then(({ room: gameRoom, user: myUser }) => {
|
||||||
user = myUser;
|
if (call.writable) {
|
||||||
} else {
|
room = gameRoom;
|
||||||
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
user = myUser;
|
||||||
socketManager.leaveRoom(gameRoom, myUser);
|
} else {
|
||||||
}
|
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
||||||
});
|
socketManager.leaveRoom(gameRoom, myUser);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error('The first message sent MUST be of type JoinRoomMessage');
|
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (message.hasJoinroommessage()) {
|
if (message.hasJoinroommessage()) {
|
||||||
throw new Error('Cannot call JoinRoomMessage twice!');
|
throw new Error("Cannot call JoinRoomMessage twice!");
|
||||||
} else if (message.hasUsermovesmessage()) {
|
} else if (message.hasUsermovesmessage()) {
|
||||||
socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage);
|
socketManager.handleUserMovesMessage(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getUsermovesmessage() as UserMovesMessage
|
||||||
|
);
|
||||||
} else if (message.hasSilentmessage()) {
|
} else if (message.hasSilentmessage()) {
|
||||||
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
||||||
} else if (message.hasItemeventmessage()) {
|
} else if (message.hasItemeventmessage()) {
|
||||||
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
|
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
|
||||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||||
socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
|
socketManager.emitVideo(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||||
|
);
|
||||||
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||||
socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
|
socketManager.emitScreenSharing(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||||
|
);
|
||||||
} else if (message.hasPlayglobalmessage()) {
|
} else if (message.hasPlayglobalmessage()) {
|
||||||
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
|
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
|
||||||
} else if (message.hasQueryjitsijwtmessage()){
|
} else if (message.hasQueryjitsijwtmessage()) {
|
||||||
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
|
socketManager.handleQueryJitsiJwtMessage(
|
||||||
} else if (message.hasEmotepromptmessage()){
|
user,
|
||||||
socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage);
|
message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
|
||||||
}else if (message.hasSendusermessage()) {
|
);
|
||||||
|
} else if (message.hasEmotepromptmessage()) {
|
||||||
|
socketManager.handleEmoteEventMessage(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getEmotepromptmessage() as EmotePromptMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasSendusermessage()) {
|
||||||
const sendUserMessage = message.getSendusermessage();
|
const sendUserMessage = message.getSendusermessage();
|
||||||
if(sendUserMessage !== undefined) {
|
if (sendUserMessage !== undefined) {
|
||||||
socketManager.handlerSendUserMessage(user, sendUserMessage);
|
socketManager.handlerSendUserMessage(user, sendUserMessage);
|
||||||
}
|
}
|
||||||
}else if (message.hasBanusermessage()) {
|
} else if (message.hasBanusermessage()) {
|
||||||
const banUserMessage = message.getBanusermessage();
|
const banUserMessage = message.getBanusermessage();
|
||||||
if(banUserMessage !== undefined) {
|
if (banUserMessage !== undefined) {
|
||||||
socketManager.handlerBanUserMessage(room, user, banUserMessage);
|
socketManager.handlerBanUserMessage(room, user, banUserMessage);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unhandled message type');
|
throw new Error("Unhandled message type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emitError(call, e);
|
emitError(call, e);
|
||||||
call.end();
|
call.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('end', () => {
|
call.on("end", () => {
|
||||||
debug('joinRoom ended');
|
debug("joinRoom ended");
|
||||||
if (user !== null && room !== null) {
|
if (user !== null && room !== null) {
|
||||||
socketManager.leaveRoom(room, user);
|
socketManager.leaveRoom(room, user);
|
||||||
}
|
}
|
||||||
@ -105,41 +127,40 @@ const roomManager: IRoomManagerServer = {
|
|||||||
user = null;
|
user = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('error', (err: Error) => {
|
call.on("error", (err: Error) => {
|
||||||
console.error('An error occurred in joinRoom stream:', err);
|
console.error("An error occurred in joinRoom stream:", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
listenZone(call: ZoneSocket): void {
|
listenZone(call: ZoneSocket): void {
|
||||||
debug('listenZone called');
|
debug("listenZone called");
|
||||||
const zoneMessage = call.request;
|
const zoneMessage = call.request;
|
||||||
|
|
||||||
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||||
|
|
||||||
call.on('cancelled', () => {
|
call.on("cancelled", () => {
|
||||||
debug('listenZone cancelled');
|
debug("listenZone cancelled");
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||||
call.end();
|
call.end();
|
||||||
})
|
});
|
||||||
|
|
||||||
call.on('close', () => {
|
call.on("close", () => {
|
||||||
debug('listenZone connection closed');
|
debug("listenZone connection closed");
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||||
}).on('error', (e) => {
|
}).on("error", (e) => {
|
||||||
console.error('An error occurred in listenZone stream:', e);
|
console.error("An error occurred in listenZone stream:", e);
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
adminRoom(call: AdminSocket): void {
|
adminRoom(call: AdminSocket): void {
|
||||||
console.log('adminRoom called');
|
console.log("adminRoom called");
|
||||||
|
|
||||||
const admin = new Admin(call);
|
const admin = new Admin(call);
|
||||||
let room: GameRoom|null = null;
|
let room: GameRoom | null = null;
|
||||||
|
|
||||||
call.on('data', (message: AdminPusherToBackMessage) => {
|
call.on("data", (message: AdminPusherToBackMessage) => {
|
||||||
try {
|
try {
|
||||||
if (room === null) {
|
if (room === null) {
|
||||||
if (message.hasSubscribetoroom()) {
|
if (message.hasSubscribetoroom()) {
|
||||||
@ -148,18 +169,17 @@ const roomManager: IRoomManagerServer = {
|
|||||||
room = gameRoom;
|
room = gameRoom;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error('The first message sent MUST be of type JoinRoomMessage');
|
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emitError(call, e);
|
emitError(call, e);
|
||||||
call.end();
|
call.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('end', () => {
|
call.on("end", () => {
|
||||||
debug('joinRoom ended');
|
debug("joinRoom ended");
|
||||||
if (room !== null) {
|
if (room !== null) {
|
||||||
socketManager.leaveAdminRoom(room, admin);
|
socketManager.leaveAdminRoom(room, admin);
|
||||||
}
|
}
|
||||||
@ -167,18 +187,21 @@ const roomManager: IRoomManagerServer = {
|
|||||||
room = null;
|
room = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('error', (err: Error) => {
|
call.on("error", (err: Error) => {
|
||||||
console.error('An error occurred in joinAdminRoom stream:', err);
|
console.error("An error occurred in joinAdminRoom stream:", err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
|
socketManager.sendAdminMessage(
|
||||||
socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage());
|
call.request.getRoomid(),
|
||||||
|
call.request.getRecipientuuid(),
|
||||||
|
call.request.getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
throw new Error('Not implemented yet');
|
throw new Error("Not implemented yet");
|
||||||
// TODO
|
// TODO
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
@ -192,14 +215,20 @@ const roomManager: IRoomManagerServer = {
|
|||||||
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
|
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendWorldFullWarningToRoom(call: ServerUnaryCall<WorldFullWarningToRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendWorldFullWarningToRoom(
|
||||||
|
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
|
||||||
|
callback: sendUnaryData<EmptyMessage>
|
||||||
|
): void {
|
||||||
socketManager.dispatchWorlFullWarning(call.request.getRoomid());
|
socketManager.dispatchWorlFullWarning(call.request.getRoomid());
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendRefreshRoomPrompt(call: ServerUnaryCall<RefreshRoomPromptMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendRefreshRoomPrompt(
|
||||||
|
call: ServerUnaryCall<RefreshRoomPromptMessage>,
|
||||||
|
callback: sendUnaryData<EmptyMessage>
|
||||||
|
): void {
|
||||||
socketManager.dispatchRoomRefresh(call.request.getRoomid());
|
socketManager.dispatchRoomRefresh(call.request.getRoomid());
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export {roomManager};
|
export { roomManager };
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { App as _App, AppOptions } from 'uWebSockets.js';
|
import { App as _App, AppOptions } from "uWebSockets.js";
|
||||||
import BaseApp from './baseapp';
|
import BaseApp from "./baseapp";
|
||||||
import { extend } from './utils';
|
import { extend } from "./utils";
|
||||||
import { UwsApp } from './types';
|
import { UwsApp } from "./types";
|
||||||
|
|
||||||
class App extends (<UwsApp>_App) {
|
class App extends (<UwsApp>_App) {
|
||||||
constructor(options: AppOptions = {}) {
|
constructor(options: AppOptions = {}) {
|
||||||
super(options); // eslint-disable-line constructor-super
|
super(options); // eslint-disable-line constructor-super
|
||||||
extend(this, new BaseApp());
|
extend(this, new BaseApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -1,116 +1,109 @@
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from "stream";
|
||||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||||
|
|
||||||
import formData from './formdata';
|
import formData from "./formdata";
|
||||||
import { stob } from './utils';
|
import { stob } from "./utils";
|
||||||
import { Handler } from './types';
|
import { Handler } from "./types";
|
||||||
import {join} from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
|
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
|
||||||
const noOp = () => true;
|
const noOp = () => true;
|
||||||
|
|
||||||
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
||||||
const contType = req.getHeader('content-type');
|
const contType = req.getHeader("content-type");
|
||||||
|
|
||||||
res.bodyStream = function() {
|
res.bodyStream = function () {
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
||||||
|
|
||||||
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
||||||
// uint and then slicing is bit faster than slice and then uint
|
// uint and then slicing is bit faster than slice and then uint
|
||||||
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
stream.push(null);
|
stream.push(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
res.body = () => stob(res.bodyStream());
|
res.body = () => stob(res.bodyStream());
|
||||||
|
|
||||||
if (contType.includes('application/json'))
|
if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
|
||||||
res.json = async () => JSON.parse(await res.body());
|
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
|
||||||
if (contTypes.map(t => contType.includes(t)).includes(true))
|
|
||||||
res.formData = formData.bind(res, contType);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class BaseApp {
|
class BaseApp {
|
||||||
_sockets = new Map();
|
_sockets = new Map();
|
||||||
ws!: TemplatedApp['ws'];
|
ws!: TemplatedApp["ws"];
|
||||||
get!: TemplatedApp['get'];
|
get!: TemplatedApp["get"];
|
||||||
_post!: TemplatedApp['post'];
|
_post!: TemplatedApp["post"];
|
||||||
_put!: TemplatedApp['put'];
|
_put!: TemplatedApp["put"];
|
||||||
_patch!: TemplatedApp['patch'];
|
_patch!: TemplatedApp["patch"];
|
||||||
_listen!: TemplatedApp['listen'];
|
_listen!: TemplatedApp["listen"];
|
||||||
|
|
||||||
post(pattern: string, handler: Handler) {
|
post(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._post(pattern, (res, req) => {
|
||||||
this._post(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
handler(res, req);
|
||||||
handler(res, req);
|
});
|
||||||
});
|
return this;
|
||||||
return this;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
put(pattern: string, handler: Handler) {
|
put(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._put(pattern, (res, req) => {
|
||||||
this._put(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
|
||||||
|
|
||||||
handler(res, req);
|
handler(res, req);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
patch(pattern: string, handler: Handler) {
|
patch(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._patch(pattern, (res, req) => {
|
||||||
this._patch(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
|
||||||
|
|
||||||
handler(res, req);
|
handler(res, req);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
||||||
if (typeof p === 'number' && typeof h === 'string') {
|
if (typeof p === "number" && typeof h === "string") {
|
||||||
this._listen(h, p, socket => {
|
this._listen(h, p, (socket) => {
|
||||||
this._sockets.set(p, socket);
|
this._sockets.set(p, socket);
|
||||||
if (cb === undefined) {
|
if (cb === undefined) {
|
||||||
throw new Error('cb undefined');
|
throw new Error("cb undefined");
|
||||||
|
}
|
||||||
|
cb(socket);
|
||||||
|
});
|
||||||
|
} else if (typeof h === "number" && typeof p === "function") {
|
||||||
|
this._listen(h, (socket) => {
|
||||||
|
this._sockets.set(h, socket);
|
||||||
|
p(socket);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
|
||||||
}
|
}
|
||||||
cb(socket);
|
|
||||||
});
|
return this;
|
||||||
} else if (typeof h === 'number' && typeof p === 'function') {
|
|
||||||
this._listen(h, socket => {
|
|
||||||
this._sockets.set(h, socket);
|
|
||||||
p(socket);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw Error(
|
|
||||||
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
close(port: null | number = null) {
|
||||||
}
|
if (port) {
|
||||||
|
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
||||||
close(port: null | number = null) {
|
this._sockets.delete(port);
|
||||||
if (port) {
|
} else {
|
||||||
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
this._sockets.forEach((app) => {
|
||||||
this._sockets.delete(port);
|
us_listen_socket_close(app);
|
||||||
} else {
|
});
|
||||||
this._sockets.forEach(app => {
|
this._sockets.clear();
|
||||||
us_listen_socket_close(app);
|
}
|
||||||
});
|
return this;
|
||||||
this._sockets.clear();
|
|
||||||
}
|
}
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BaseApp;
|
export default BaseApp;
|
||||||
|
@ -1,100 +1,99 @@
|
|||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from "fs";
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from "path";
|
||||||
import Busboy from 'busboy';
|
import Busboy from "busboy";
|
||||||
import mkdirp from 'mkdirp';
|
import mkdirp from "mkdirp";
|
||||||
|
|
||||||
function formData(
|
function formData(
|
||||||
contType: string,
|
contType: string,
|
||||||
options: busboy.BusboyConfig & {
|
options: busboy.BusboyConfig & {
|
||||||
abortOnLimit?: boolean;
|
abortOnLimit?: boolean;
|
||||||
tmpDir?: string;
|
tmpDir?: string;
|
||||||
onFile?: (
|
onFile?: (
|
||||||
fieldname: string,
|
fieldname: string,
|
||||||
file: NodeJS.ReadableStream,
|
file: NodeJS.ReadableStream,
|
||||||
filename: string,
|
filename: string,
|
||||||
encoding: string,
|
encoding: string,
|
||||||
mimetype: string
|
mimetype: string
|
||||||
) => string;
|
) => string;
|
||||||
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
filename?: (oldName: string) => string;
|
filename?: (oldName: string) => string;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
console.log('Enter form data');
|
console.log("Enter form data");
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'content-type': contType
|
"content-type": contType,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const busb = new Busboy(options);
|
const busb = new Busboy(options);
|
||||||
const ret = {};
|
const ret = {};
|
||||||
|
|
||||||
this.bodyStream().pipe(busb);
|
this.bodyStream().pipe(busb);
|
||||||
|
|
||||||
busb.on('limit', () => {
|
busb.on("limit", () => {
|
||||||
if (options.abortOnLimit) {
|
if (options.abortOnLimit) {
|
||||||
reject(Error('limit'));
|
reject(Error("limit"));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
|
||||||
|
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
|
||||||
|
filename,
|
||||||
|
encoding,
|
||||||
|
mimetype,
|
||||||
|
filePath: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof options.tmpDir === "string") {
|
||||||
|
if (typeof options.filename === "function") filename = options.filename(filename);
|
||||||
|
const fileToSave = join(options.tmpDir, filename);
|
||||||
|
mkdirp(dirname(fileToSave));
|
||||||
|
|
||||||
|
file.pipe(createWriteStream(fileToSave));
|
||||||
|
value.filePath = fileToSave;
|
||||||
|
}
|
||||||
|
if (typeof options.onFile === "function") {
|
||||||
|
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRetValue(ret, fieldname, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("field", function (fieldname, value) {
|
||||||
|
if (typeof options.onField === "function") options.onField(fieldname, value);
|
||||||
|
|
||||||
|
setRetValue(ret, fieldname, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("finish", function () {
|
||||||
|
resolve(ret);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("error", reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
|
||||||
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = {
|
|
||||||
filename,
|
|
||||||
encoding,
|
|
||||||
mimetype,
|
|
||||||
filePath: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof options.tmpDir === 'string') {
|
|
||||||
if (typeof options.filename === 'function') filename = options.filename(filename);
|
|
||||||
const fileToSave = join(options.tmpDir, filename);
|
|
||||||
mkdirp(dirname(fileToSave));
|
|
||||||
|
|
||||||
file.pipe(createWriteStream(fileToSave));
|
|
||||||
value.filePath = fileToSave;
|
|
||||||
}
|
|
||||||
if (typeof options.onFile === 'function') {
|
|
||||||
value.filePath =
|
|
||||||
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRetValue(ret, fieldname, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('field', function(fieldname, value) {
|
|
||||||
if (typeof options.onField === 'function') options.onField(fieldname, value);
|
|
||||||
|
|
||||||
setRetValue(ret, fieldname, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('finish', function() {
|
|
||||||
resolve(ret);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRetValue(
|
function setRetValue(
|
||||||
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
fieldname: string,
|
fieldname: string,
|
||||||
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
) {
|
) {
|
||||||
if (fieldname.endsWith('[]')) {
|
if (fieldname.endsWith("[]")) {
|
||||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
fieldname = fieldname.slice(0, fieldname.length - 2);
|
||||||
if (Array.isArray(ret[fieldname])) {
|
if (Array.isArray(ret[fieldname])) {
|
||||||
ret[fieldname].push(value);
|
ret[fieldname].push(value);
|
||||||
|
} else {
|
||||||
|
ret[fieldname] = [value];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ret[fieldname] = [value];
|
if (Array.isArray(ret[fieldname])) {
|
||||||
|
ret[fieldname].push(value);
|
||||||
|
} else if (ret[fieldname]) {
|
||||||
|
ret[fieldname] = [ret[fieldname], value];
|
||||||
|
} else {
|
||||||
|
ret[fieldname] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (Array.isArray(ret[fieldname])) {
|
|
||||||
ret[fieldname].push(value);
|
|
||||||
} else if (ret[fieldname]) {
|
|
||||||
ret[fieldname] = [ret[fieldname], value];
|
|
||||||
} else {
|
|
||||||
ret[fieldname] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default formData;
|
export default formData;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js';
|
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
|
||||||
import BaseApp from './baseapp';
|
import BaseApp from "./baseapp";
|
||||||
import { extend } from './utils';
|
import { extend } from "./utils";
|
||||||
import { UwsApp } from './types';
|
import { UwsApp } from "./types";
|
||||||
|
|
||||||
class SSLApp extends (<UwsApp>_SSLApp) {
|
class SSLApp extends (<UwsApp>_SSLApp) {
|
||||||
constructor(options: AppOptions) {
|
constructor(options: AppOptions) {
|
||||||
super(options); // eslint-disable-line constructor-super
|
super(options); // eslint-disable-line constructor-super
|
||||||
extend(this, new BaseApp());
|
extend(this, new BaseApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SSLApp;
|
export default SSLApp;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||||
|
|
||||||
export type UwsApp = {
|
export type UwsApp = {
|
||||||
(options: AppOptions): TemplatedApp;
|
(options: AppOptions): TemplatedApp;
|
||||||
new (options: AppOptions): TemplatedApp;
|
new (options: AppOptions): TemplatedApp;
|
||||||
prototype: TemplatedApp;
|
prototype: TemplatedApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
||||||
|
@ -1,37 +1,36 @@
|
|||||||
import { ReadStream } from 'fs';
|
import { ReadStream } from "fs";
|
||||||
|
|
||||||
function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(
|
function extend(who: any, from: any, overwrite = true) {
|
||||||
Object.keys(from)
|
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
|
||||||
);
|
ownProps.forEach((prop) => {
|
||||||
ownProps.forEach(prop => {
|
if (prop === "constructor" || from[prop] === undefined) return;
|
||||||
if (prop === 'constructor' || from[prop] === undefined) return;
|
if (who[prop] && overwrite) {
|
||||||
if (who[prop] && overwrite) {
|
who[`_${prop}`] = who[prop];
|
||||||
who[`_${prop}`] = who[prop];
|
}
|
||||||
}
|
if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
|
||||||
if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who);
|
else who[prop] = from[prop];
|
||||||
else who[prop] = from[prop];
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stob(stream: ReadStream): Promise<Buffer> {
|
function stob(stream: ReadStream): Promise<Buffer> {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
const buffers: Buffer[] = [];
|
const buffers: Buffer[] = [];
|
||||||
stream.on('data', buffers.push.bind(buffers));
|
stream.on("data", buffers.push.bind(buffers));
|
||||||
|
|
||||||
stream.on('end', () => {
|
stream.on("end", () => {
|
||||||
switch (buffers.length) {
|
switch (buffers.length) {
|
||||||
case 0:
|
case 0:
|
||||||
resolve(Buffer.allocUnsafe(0));
|
resolve(Buffer.allocUnsafe(0));
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
resolve(buffers[0]);
|
resolve(buffers[0]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
resolve(Buffer.concat(buffers));
|
resolve(Buffer.concat(buffers));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { extend, stob };
|
export { extend, stob };
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { parse } from 'query-string';
|
import { parse } from "query-string";
|
||||||
import { HttpRequest } from 'uWebSockets.js';
|
import { HttpRequest } from "uWebSockets.js";
|
||||||
import App from './server/app';
|
import App from "./server/app";
|
||||||
import SSLApp from './server/sslapp';
|
import SSLApp from "./server/sslapp";
|
||||||
import * as types from './server/types';
|
import * as types from "./server/types";
|
||||||
|
|
||||||
const getQuery = (req: HttpRequest) => {
|
const getQuery = (req: HttpRequest) => {
|
||||||
return parse(req.getQuery());
|
return parse(req.getQuery());
|
||||||
};
|
};
|
||||||
|
|
||||||
export { App, SSLApp, getQuery };
|
export { App, SSLApp, getQuery };
|
||||||
export * from './server/types';
|
export * from "./server/types";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
App,
|
App,
|
||||||
SSLApp,
|
SSLApp,
|
||||||
getQuery,
|
getQuery,
|
||||||
...types
|
...types,
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export const arrayIntersect = (array1: string[], array2: string[]) : boolean => {
|
export const arrayIntersect = (array1: string[], array2: string[]): boolean => {
|
||||||
return array1.filter(value => array2.includes(value)).length > 0;
|
return array1.filter((value) => array2.includes(value)).length > 0;
|
||||||
}
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require("events");
|
||||||
|
|
||||||
const clientJoinEvent = 'clientJoin';
|
const clientJoinEvent = "clientJoin";
|
||||||
const clientLeaveEvent = 'clientLeave';
|
const clientLeaveEvent = "clientLeave";
|
||||||
|
|
||||||
class ClientEventsEmitter extends EventEmitter {
|
class ClientEventsEmitter extends EventEmitter {
|
||||||
emitClientJoin(clientUUid: string, roomId: string): void {
|
emitClientJoin(clientUUid: string, roomId: string): void {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable";
|
import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
function secNSec2ms(secNSec: Array<number>|number) {
|
function secNSec2ms(secNSec: Array<number> | number) {
|
||||||
if (Array.isArray(secNSec)) {
|
if (Array.isArray(secNSec)) {
|
||||||
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
||||||
}
|
}
|
||||||
@ -12,17 +12,17 @@ class CpuTracker {
|
|||||||
private overHeating: boolean = false;
|
private overHeating: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let time = process.hrtime.bigint()
|
let time = process.hrtime.bigint();
|
||||||
let usage = process.cpuUsage()
|
let usage = process.cpuUsage();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const elapTime = process.hrtime.bigint();
|
const elapTime = process.hrtime.bigint();
|
||||||
const elapUsage = process.cpuUsage(usage)
|
const elapUsage = process.cpuUsage(usage);
|
||||||
usage = process.cpuUsage()
|
usage = process.cpuUsage();
|
||||||
|
|
||||||
const elapTimeMS = elapTime - time;
|
const elapTimeMS = elapTime - time;
|
||||||
const elapUserMS = secNSec2ms(elapUsage.user)
|
const elapUserMS = secNSec2ms(elapUsage.user);
|
||||||
const elapSystMS = secNSec2ms(elapUsage.system)
|
const elapSystMS = secNSec2ms(elapUsage.system);
|
||||||
this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000)
|
this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
|
||||||
|
|
||||||
time = elapTime;
|
time = elapTime;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Counter, Gauge} from "prom-client";
|
import { Counter, Gauge } from "prom-client";
|
||||||
|
|
||||||
//this class should manage all the custom metrics used by prometheus
|
//this class should manage all the custom metrics used by prometheus
|
||||||
class GaugeManager {
|
class GaugeManager {
|
||||||
@ -10,29 +10,29 @@ class GaugeManager {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.nbRoomsGauge = new Gauge({
|
this.nbRoomsGauge = new Gauge({
|
||||||
name: 'workadventure_nb_rooms',
|
name: "workadventure_nb_rooms",
|
||||||
help: 'Number of active rooms'
|
help: "Number of active rooms",
|
||||||
});
|
});
|
||||||
this.nbClientsGauge = new Gauge({
|
this.nbClientsGauge = new Gauge({
|
||||||
name: 'workadventure_nb_sockets',
|
name: "workadventure_nb_sockets",
|
||||||
help: 'Number of connected sockets',
|
help: "Number of connected sockets",
|
||||||
labelNames: [ ]
|
labelNames: [],
|
||||||
});
|
});
|
||||||
this.nbClientsPerRoomGauge = new Gauge({
|
this.nbClientsPerRoomGauge = new Gauge({
|
||||||
name: 'workadventure_nb_clients_per_room',
|
name: "workadventure_nb_clients_per_room",
|
||||||
help: 'Number of clients per room',
|
help: "Number of clients per room",
|
||||||
labelNames: [ 'room' ]
|
labelNames: ["room"],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nbGroupsPerRoomCounter = new Counter({
|
this.nbGroupsPerRoomCounter = new Counter({
|
||||||
name: 'workadventure_counter_groups_per_room',
|
name: "workadventure_counter_groups_per_room",
|
||||||
help: 'Counter of groups per room',
|
help: "Counter of groups per room",
|
||||||
labelNames: [ 'room' ]
|
labelNames: ["room"],
|
||||||
});
|
});
|
||||||
this.nbGroupsPerRoomGauge = new Gauge({
|
this.nbGroupsPerRoomGauge = new Gauge({
|
||||||
name: 'workadventure_nb_groups_per_room',
|
name: "workadventure_nb_groups_per_room",
|
||||||
help: 'Number of groups per room',
|
help: "Number of groups per room",
|
||||||
labelNames: [ 'room' ]
|
labelNames: ["room"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,13 +54,13 @@ class GaugeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
incNbGroupsPerRoomGauge(roomId: string): void {
|
incNbGroupsPerRoomGauge(roomId: string): void {
|
||||||
this.nbGroupsPerRoomCounter.inc({ room: roomId })
|
this.nbGroupsPerRoomCounter.inc({ room: roomId });
|
||||||
this.nbGroupsPerRoomGauge.inc({ room: roomId })
|
this.nbGroupsPerRoomGauge.inc({ room: roomId });
|
||||||
}
|
}
|
||||||
|
|
||||||
decNbGroupsPerRoomGauge(roomId: string): void {
|
decNbGroupsPerRoomGauge(roomId: string): void {
|
||||||
this.nbGroupsPerRoomGauge.dec({ room: roomId })
|
this.nbGroupsPerRoomGauge.dec({ room: roomId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const gaugeManager = new GaugeManager();
|
export const gaugeManager = new GaugeManager();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb";
|
import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb";
|
||||||
import {UserSocket} from "_Model/User";
|
import { UserSocket } from "_Model/User";
|
||||||
|
|
||||||
export function emitError(Client: UserSocket, message: string): void {
|
export function emitError(Client: UserSocket, message: string): void {
|
||||||
const errorMessage = new ErrorMessage();
|
const errorMessage = new ErrorMessage();
|
||||||
@ -9,7 +9,7 @@ export function emitError(Client: UserSocket, message: string): void {
|
|||||||
serverToClientMessage.setErrormessage(errorMessage);
|
serverToClientMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
//if (!Client.disconnecting) {
|
//if (!Client.disconnecting) {
|
||||||
Client.write(serverToClientMessage);
|
Client.write(serverToClientMessage);
|
||||||
//}
|
//}
|
||||||
console.warn(message);
|
console.warn(message);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {GameRoom} from "../Model/GameRoom";
|
import { GameRoom } from "../Model/GameRoom";
|
||||||
import {
|
import {
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
ItemStateMessage,
|
ItemStateMessage,
|
||||||
@ -27,39 +27,39 @@ import {
|
|||||||
WorldFullWarningMessage,
|
WorldFullWarningMessage,
|
||||||
UserLeftZoneMessage,
|
UserLeftZoneMessage,
|
||||||
EmoteEventMessage,
|
EmoteEventMessage,
|
||||||
BanUserMessage, RefreshRoomMessage, EmotePromptMessage,
|
BanUserMessage,
|
||||||
|
RefreshRoomMessage,
|
||||||
|
EmotePromptMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import {User, UserSocket} from "../Model/User";
|
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 {
|
import {
|
||||||
GROUP_RADIUS,
|
GROUP_RADIUS,
|
||||||
JITSI_ISS,
|
JITSI_ISS,
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
SECRET_JITSI_KEY,
|
SECRET_JITSI_KEY,
|
||||||
TURN_STATIC_AUTH_SECRET
|
TURN_STATIC_AUTH_SECRET,
|
||||||
} from "../Enum/EnvironmentVariable";
|
} from "../Enum/EnvironmentVariable";
|
||||||
import {Movable} from "../Model/Movable";
|
import { Movable } from "../Model/Movable";
|
||||||
import {PositionInterface} from "../Model/PositionInterface";
|
import { PositionInterface } from "../Model/PositionInterface";
|
||||||
import Jwt from "jsonwebtoken";
|
import Jwt from "jsonwebtoken";
|
||||||
import {JITSI_URL} from "../Enum/EnvironmentVariable";
|
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
||||||
import {clientEventsEmitter} from "./ClientEventsEmitter";
|
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
||||||
import {gaugeManager} from "./GaugeManager";
|
import { gaugeManager } from "./GaugeManager";
|
||||||
import {ZoneSocket} from "../RoomManager";
|
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";
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
const debug = Debug("sockermanager");
|
||||||
const debug = Debug('sockermanager');
|
|
||||||
|
|
||||||
function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void {
|
function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void {
|
||||||
// TODO: should we batch those every 100ms?
|
// TODO: should we batch those every 100ms?
|
||||||
const batchMessage = new BatchToPusherMessage();
|
const batchMessage = new BatchToPusherMessage();
|
||||||
batchMessage.addPayload(subMessage);
|
batchMessage.addPayload(subMessage);
|
||||||
|
|
||||||
|
|
||||||
socket.write(batchMessage);
|
socket.write(batchMessage);
|
||||||
}
|
}
|
||||||
@ -68,7 +68,6 @@ export class SocketManager {
|
|||||||
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
|
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
||||||
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
||||||
gaugeManager.incNbClientPerRoomGauge(roomId);
|
gaugeManager.incNbClientPerRoomGauge(roomId);
|
||||||
});
|
});
|
||||||
@ -77,16 +76,18 @@ export class SocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> {
|
public async handleJoinRoom(
|
||||||
|
socket: UserSocket,
|
||||||
|
joinRoomMessage: JoinRoomMessage
|
||||||
|
): Promise<{ room: GameRoom; user: User }> {
|
||||||
//join new previous room
|
//join new previous room
|
||||||
const {room, user} = await this.joinRoom(socket, joinRoomMessage);
|
const { room, user } = await this.joinRoom(socket, joinRoomMessage);
|
||||||
|
|
||||||
if (!socket.writable) {
|
if (!socket.writable) {
|
||||||
console.warn('Socket was aborted');
|
console.warn("Socket was aborted");
|
||||||
return {
|
return {
|
||||||
room,
|
room,
|
||||||
user
|
user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const roomJoinedMessage = new RoomJoinedMessage();
|
const roomJoinedMessage = new RoomJoinedMessage();
|
||||||
@ -108,9 +109,8 @@ export class SocketManager {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
room,
|
room,
|
||||||
user
|
user,
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
||||||
@ -124,13 +124,12 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (position === undefined) {
|
if (position === undefined) {
|
||||||
throw new Error('Position not found in message');
|
throw new Error("Position not found in message");
|
||||||
}
|
}
|
||||||
const viewport = userMoves.viewport;
|
const viewport = userMoves.viewport;
|
||||||
if (viewport === undefined) {
|
if (viewport === undefined) {
|
||||||
throw new Error('Viewport not found in message');
|
throw new Error("Viewport not found in message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// update position in the world
|
// update position in the world
|
||||||
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
||||||
@ -189,7 +188,11 @@ export class SocketManager {
|
|||||||
//send only at user
|
//send only at user
|
||||||
const remoteUser = room.getUsers().get(data.getReceiverid());
|
const remoteUser = room.getUsers().get(data.getReceiverid());
|
||||||
if (remoteUser === undefined) {
|
if (remoteUser === undefined) {
|
||||||
console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition.");
|
console.warn(
|
||||||
|
"While exchanging a WebRTC signal: client with id ",
|
||||||
|
data.getReceiverid(),
|
||||||
|
" does not exist. This might be a race condition."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,8 +200,8 @@ export class SocketManager {
|
|||||||
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"
|
// TODO: only compute credentials if data.signal.type === "offer"
|
||||||
if (TURN_STATIC_AUTH_SECRET !== '') {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
||||||
webrtcSignalToClient.setWebrtcusername(username);
|
webrtcSignalToClient.setWebrtcusername(username);
|
||||||
webrtcSignalToClient.setWebrtcpassword(password);
|
webrtcSignalToClient.setWebrtcpassword(password);
|
||||||
}
|
}
|
||||||
@ -207,7 +210,7 @@ export class SocketManager {
|
|||||||
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
|
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
|
||||||
|
|
||||||
//if (!client.disconnecting) {
|
//if (!client.disconnecting) {
|
||||||
remoteUser.socket.write(serverToClientMessage);
|
remoteUser.socket.write(serverToClientMessage);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +218,11 @@ export class SocketManager {
|
|||||||
//send only at user
|
//send only at user
|
||||||
const remoteUser = room.getUsers().get(data.getReceiverid());
|
const remoteUser = room.getUsers().get(data.getReceiverid());
|
||||||
if (remoteUser === undefined) {
|
if (remoteUser === undefined) {
|
||||||
console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition.");
|
console.warn(
|
||||||
|
"While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ",
|
||||||
|
data.getReceiverid(),
|
||||||
|
" does not exist. This might be a race condition."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,8 +230,8 @@ export class SocketManager {
|
|||||||
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"
|
// TODO: only compute credentials if data.signal.type === "offer"
|
||||||
if (TURN_STATIC_AUTH_SECRET !== '') {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
||||||
webrtcSignalToClient.setWebrtcusername(username);
|
webrtcSignalToClient.setWebrtcusername(username);
|
||||||
webrtcSignalToClient.setWebrtcpassword(password);
|
webrtcSignalToClient.setWebrtcpassword(password);
|
||||||
}
|
}
|
||||||
@ -233,11 +240,11 @@ export class SocketManager {
|
|||||||
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
|
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
|
||||||
|
|
||||||
//if (!client.disconnecting) {
|
//if (!client.disconnecting) {
|
||||||
remoteUser.socket.write(serverToClientMessage);
|
remoteUser.socket.write(serverToClientMessage);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveRoom(room: GameRoom, user: User){
|
leaveRoom(room: GameRoom, user: User) {
|
||||||
// leave previous room and world
|
// leave previous room and world
|
||||||
try {
|
try {
|
||||||
//user leave previous world
|
//user leave previous world
|
||||||
@ -249,33 +256,39 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
|
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
|
||||||
console.log('A user left');
|
console.log("A user left");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
||||||
//check and create new world for a room
|
//check and create new world for a room
|
||||||
let world = this.rooms.get(roomId)
|
let world = this.rooms.get(roomId);
|
||||||
if(world === undefined){
|
if (world === undefined) {
|
||||||
world = new GameRoom(
|
world = new GameRoom(
|
||||||
roomId,
|
roomId,
|
||||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
||||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
(user: User, group: Group) => this.disConnectedUser(user, group),
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
GROUP_RADIUS,
|
GROUP_RADIUS,
|
||||||
(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener),
|
(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
|
||||||
(thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener),
|
this.onZoneEnter(thing, fromZone, listener),
|
||||||
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener),
|
(thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
|
||||||
(emoteEventMessage:EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener),
|
this.onClientMove(thing, position, listener),
|
||||||
|
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
||||||
|
this.onClientLeave(thing, newZone, listener),
|
||||||
|
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||||
|
this.onEmote(emoteEventMessage, listener)
|
||||||
);
|
);
|
||||||
gaugeManager.incNbRoomGauge();
|
gaugeManager.incNbRoomGauge();
|
||||||
this.rooms.set(roomId, world);
|
this.rooms.set(roomId, world);
|
||||||
}
|
}
|
||||||
return Promise.resolve(world)
|
return Promise.resolve(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async joinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> {
|
private async joinRoom(
|
||||||
|
socket: UserSocket,
|
||||||
|
joinRoomMessage: JoinRoomMessage
|
||||||
|
): Promise<{ room: GameRoom; user: User }> {
|
||||||
const roomId = joinRoomMessage.getRoomid();
|
const roomId = joinRoomMessage.getRoomid();
|
||||||
|
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
const room = await socketManager.getOrCreateRoom(roomId);
|
||||||
@ -284,15 +297,15 @@ export class SocketManager {
|
|||||||
const user = room.join(socket, joinRoomMessage);
|
const user = room.join(socket, joinRoomMessage);
|
||||||
|
|
||||||
clientEventsEmitter.emitClientJoin(user.uuid, roomId);
|
clientEventsEmitter.emitClientJoin(user.uuid, roomId);
|
||||||
console.log(new Date().toISOString() + ' A user joined');
|
console.log(new Date().toISOString() + " A user joined");
|
||||||
return {room, user};
|
return { room, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) {
|
private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) {
|
||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
const userJoinedZoneMessage = new UserJoinedZoneMessage();
|
const userJoinedZoneMessage = new UserJoinedZoneMessage();
|
||||||
if (!Number.isInteger(thing.id)) {
|
if (!Number.isInteger(thing.id)) {
|
||||||
throw new Error('clientUser.userId is not an integer '+thing.id);
|
throw new Error("clientUser.userId is not an integer " + thing.id);
|
||||||
}
|
}
|
||||||
userJoinedZoneMessage.setUserid(thing.id);
|
userJoinedZoneMessage.setUserid(thing.id);
|
||||||
userJoinedZoneMessage.setName(thing.name);
|
userJoinedZoneMessage.setName(thing.name);
|
||||||
@ -312,11 +325,11 @@ export class SocketManager {
|
|||||||
} else if (thing instanceof Group) {
|
} else if (thing instanceof Group) {
|
||||||
this.emitCreateUpdateGroupEvent(listener, fromZone, thing);
|
this.emitCreateUpdateGroupEvent(listener, fromZone, thing);
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected type for Movable.');
|
console.error("Unexpected type for Movable.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClientMove(thing: Movable, position:PositionInterface, listener: ZoneSocket): void {
|
private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void {
|
||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
const userMovedMessage = new UserMovedMessage();
|
const userMovedMessage = new UserMovedMessage();
|
||||||
userMovedMessage.setUserid(thing.id);
|
userMovedMessage.setUserid(thing.id);
|
||||||
@ -331,21 +344,20 @@ export class SocketManager {
|
|||||||
} else if (thing instanceof Group) {
|
} else if (thing instanceof Group) {
|
||||||
this.emitCreateUpdateGroupEvent(listener, null, thing);
|
this.emitCreateUpdateGroupEvent(listener, null, thing);
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected type for Movable.');
|
console.error("Unexpected type for Movable.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClientLeave(thing: Movable, newZone: Zone|null, listener: ZoneSocket) {
|
private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) {
|
||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
this.emitUserLeftEvent(listener, thing.id, newZone);
|
this.emitUserLeftEvent(listener, thing.id, newZone);
|
||||||
} else if (thing instanceof Group) {
|
} else if (thing instanceof Group) {
|
||||||
this.emitDeleteGroupEvent(listener, thing.getId(), newZone);
|
this.emitDeleteGroupEvent(listener, thing.getId(), newZone);
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected type for Movable.');
|
console.error("Unexpected type for Movable.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) {
|
private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) {
|
||||||
const subMessage = new SubToPusherMessage();
|
const subMessage = new SubToPusherMessage();
|
||||||
subMessage.setEmoteeventmessage(emoteEventMessage);
|
subMessage.setEmoteeventmessage(emoteEventMessage);
|
||||||
@ -353,7 +365,7 @@ export class SocketManager {
|
|||||||
emitZoneMessage(subMessage, client);
|
emitZoneMessage(subMessage, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void {
|
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
|
||||||
const position = group.getPosition();
|
const position = group.getPosition();
|
||||||
const pointMessage = new PointMessage();
|
const pointMessage = new PointMessage();
|
||||||
pointMessage.setX(Math.floor(position.x));
|
pointMessage.setX(Math.floor(position.x));
|
||||||
@ -371,7 +383,7 @@ export class SocketManager {
|
|||||||
//client.emitInBatch(subMessage);
|
//client.emitInBatch(subMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone|null): void {
|
private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void {
|
||||||
const groupDeleteMessage = new GroupLeftZoneMessage();
|
const groupDeleteMessage = new GroupLeftZoneMessage();
|
||||||
groupDeleteMessage.setGroupid(groupId);
|
groupDeleteMessage.setGroupid(groupId);
|
||||||
groupDeleteMessage.setTozone(this.toProtoZone(newZone));
|
groupDeleteMessage.setTozone(this.toProtoZone(newZone));
|
||||||
@ -383,7 +395,7 @@ export class SocketManager {
|
|||||||
//user.emitInBatch(subMessage);
|
//user.emitInBatch(subMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone|null): void {
|
private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void {
|
||||||
const userLeftMessage = new UserLeftZoneMessage();
|
const userLeftMessage = new UserLeftZoneMessage();
|
||||||
userLeftMessage.setUserid(userId);
|
userLeftMessage.setUserid(userId);
|
||||||
userLeftMessage.setTozone(this.toProtoZone(newZone));
|
userLeftMessage.setTozone(this.toProtoZone(newZone));
|
||||||
@ -394,7 +406,7 @@ export class SocketManager {
|
|||||||
emitZoneMessage(subMessage, client);
|
emitZoneMessage(subMessage, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toProtoZone(zone: Zone|null): ProtoZone|undefined {
|
private toProtoZone(zone: Zone | null): ProtoZone | undefined {
|
||||||
if (zone !== null) {
|
if (zone !== null) {
|
||||||
const zoneMessage = new ProtoZone();
|
const zoneMessage = new ProtoZone();
|
||||||
zoneMessage.setX(zone.x);
|
zoneMessage.setX(zone.x);
|
||||||
@ -405,7 +417,6 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private joinWebRtcRoom(user: User, group: Group) {
|
private joinWebRtcRoom(user: User, group: Group) {
|
||||||
|
|
||||||
for (const otherUser of group.getUsers()) {
|
for (const otherUser of group.getUsers()) {
|
||||||
if (user === otherUser) {
|
if (user === otherUser) {
|
||||||
continue;
|
continue;
|
||||||
@ -416,8 +427,8 @@ 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 !== '') {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
|
||||||
webrtcStartMessage1.setWebrtcusername(username);
|
webrtcStartMessage1.setWebrtcusername(username);
|
||||||
webrtcStartMessage1.setWebrtcpassword(password);
|
webrtcStartMessage1.setWebrtcpassword(password);
|
||||||
}
|
}
|
||||||
@ -426,16 +437,16 @@ export class SocketManager {
|
|||||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||||
|
|
||||||
//if (!user.socket.disconnecting) {
|
//if (!user.socket.disconnecting) {
|
||||||
user.socket.write(serverToClientMessage1);
|
user.socket.write(serverToClientMessage1);
|
||||||
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
||||||
//}
|
//}
|
||||||
|
|
||||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
const webrtcStartMessage2 = new WebRtcStartMessage();
|
||||||
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 !== '') {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
||||||
webrtcStartMessage2.setWebrtcusername(username);
|
webrtcStartMessage2.setWebrtcusername(username);
|
||||||
webrtcStartMessage2.setWebrtcpassword(password);
|
webrtcStartMessage2.setWebrtcpassword(password);
|
||||||
}
|
}
|
||||||
@ -444,10 +455,9 @@ export class SocketManager {
|
|||||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
||||||
|
|
||||||
//if (!otherUser.socket.disconnecting) {
|
//if (!otherUser.socket.disconnecting) {
|
||||||
otherUser.socket.write(serverToClientMessage2);
|
otherUser.socket.write(serverToClientMessage2);
|
||||||
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
||||||
//}
|
//}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,17 +466,17 @@ export class SocketManager {
|
|||||||
* and the Coturn server.
|
* and the Coturn server.
|
||||||
* The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
|
* 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} {
|
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 unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours
|
||||||
const username = [unixTimeStamp, name].join(':');
|
const username = [unixTimeStamp, name].join(":");
|
||||||
const hmac = crypto.createHmac('sha1', secret);
|
const hmac = crypto.createHmac("sha1", secret);
|
||||||
hmac.setEncoding('base64');
|
hmac.setEncoding("base64");
|
||||||
hmac.write(username);
|
hmac.write(username);
|
||||||
hmac.end();
|
hmac.end();
|
||||||
const password = hmac.read();
|
const password = hmac.read();
|
||||||
return {
|
return {
|
||||||
username: username,
|
username: username,
|
||||||
password: password
|
password: password,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -489,10 +499,9 @@ export class SocketManager {
|
|||||||
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
|
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
|
||||||
|
|
||||||
//if (!otherUser.socket.disconnecting) {
|
//if (!otherUser.socket.disconnecting) {
|
||||||
otherUser.socket.write(serverToClientMessage1);
|
otherUser.socket.write(serverToClientMessage1);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
|
||||||
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
|
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
|
||||||
webrtcDisconnectMessage2.setUserid(otherUser.id);
|
webrtcDisconnectMessage2.setUserid(otherUser.id);
|
||||||
|
|
||||||
@ -500,7 +509,7 @@ export class SocketManager {
|
|||||||
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
|
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
|
||||||
|
|
||||||
//if (!user.socket.disconnecting) {
|
//if (!user.socket.disconnecting) {
|
||||||
user.socket.write(serverToClientMessage2);
|
user.socket.write(serverToClientMessage2);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -517,40 +526,41 @@ export class SocketManager {
|
|||||||
console.error('An error occurred on "emitPlayGlobalMessage" event');
|
console.error('An error occurred on "emitPlayGlobalMessage" event');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWorlds(): Map<string, GameRoom> {
|
public getWorlds(): Map<string, GameRoom> {
|
||||||
return this.rooms;
|
return this.rooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
||||||
const room = queryJitsiJwtMessage.getJitsiroom();
|
const room = queryJitsiJwtMessage.getJitsiroom();
|
||||||
const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead.
|
const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead.
|
||||||
|
|
||||||
if (SECRET_JITSI_KEY === '') {
|
if (SECRET_JITSI_KEY === "") {
|
||||||
throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.');
|
throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's see if the current client has
|
// Let's see if the current client has
|
||||||
const isAdmin = user.tags.includes(tag);
|
const isAdmin = user.tags.includes(tag);
|
||||||
|
|
||||||
const jwt = Jwt.sign({
|
const jwt = Jwt.sign(
|
||||||
"aud": "jitsi",
|
{
|
||||||
"iss": JITSI_ISS,
|
aud: "jitsi",
|
||||||
"sub": JITSI_URL,
|
iss: JITSI_ISS,
|
||||||
"room": room,
|
sub: JITSI_URL,
|
||||||
"moderator": isAdmin
|
room: room,
|
||||||
}, SECRET_JITSI_KEY, {
|
moderator: isAdmin,
|
||||||
expiresIn: '1d',
|
},
|
||||||
algorithm: "HS256",
|
SECRET_JITSI_KEY,
|
||||||
header:
|
{
|
||||||
{
|
expiresIn: "1d",
|
||||||
"alg": "HS256",
|
algorithm: "HS256",
|
||||||
"typ": "JWT"
|
header: {
|
||||||
}
|
alg: "HS256",
|
||||||
});
|
typ: "JWT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const sendJitsiJwtMessage = new SendJitsiJwtMessage();
|
const sendJitsiJwtMessage = new SendJitsiJwtMessage();
|
||||||
sendJitsiJwtMessage.setJitsiroom(room);
|
sendJitsiJwtMessage.setJitsiroom(room);
|
||||||
@ -562,7 +572,7 @@ export class SocketManager {
|
|||||||
user.socket.write(serverToClientMessage);
|
user.socket.write(serverToClientMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){
|
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
|
||||||
const sendUserMessage = new SendUserMessage();
|
const sendUserMessage = new SendUserMessage();
|
||||||
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
|
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
|
||||||
sendUserMessage.setType(sendUserMessageToSend.getType());
|
sendUserMessage.setType(sendUserMessageToSend.getType());
|
||||||
@ -572,7 +582,7 @@ export class SocketManager {
|
|||||||
user.socket.write(serverToClientMessage);
|
user.socket.write(serverToClientMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){
|
public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) {
|
||||||
const banUserMessage = new BanUserMessage();
|
const banUserMessage = new BanUserMessage();
|
||||||
banUserMessage.setMessage(banUserMessageToSend.getMessage());
|
banUserMessage.setMessage(banUserMessageToSend.getMessage());
|
||||||
banUserMessage.setType(banUserMessageToSend.getType());
|
banUserMessage.setType(banUserMessageToSend.getType());
|
||||||
@ -592,7 +602,7 @@ export class SocketManager {
|
|||||||
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
|
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
|
||||||
const room = this.rooms.get(roomId);
|
const room = this.rooms.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In addZoneListener, could not find room with id '" + roomId + "'");
|
console.error("In addZoneListener, could not find room with id '" + roomId + "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -636,7 +646,7 @@ export class SocketManager {
|
|||||||
removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) {
|
removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) {
|
||||||
const room = this.rooms.get(roomId);
|
const room = this.rooms.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
console.error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -651,7 +661,7 @@ export class SocketManager {
|
|||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
public leaveAdminRoom(room: GameRoom, admin: Admin){
|
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||||
room.adminLeave(admin);
|
room.adminLeave(admin);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomId);
|
this.rooms.delete(room.roomId);
|
||||||
@ -663,19 +673,27 @@ export class SocketManager {
|
|||||||
public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void {
|
public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void {
|
||||||
const room = this.rooms.get(roomId);
|
const room = this.rooms.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In sendAdminMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In sendAdminMessage, could not find room with id '" +
|
||||||
|
roomId +
|
||||||
|
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipient = room.getUserByUuid(recipientUuid);
|
const recipient = room.getUserByUuid(recipientUuid);
|
||||||
if (recipient === undefined) {
|
if (recipient === undefined) {
|
||||||
console.error("In sendAdminMessage, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In sendAdminMessage, could not find user with id '" +
|
||||||
|
recipientUuid +
|
||||||
|
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendUserMessage = new SendUserMessage();
|
const sendUserMessage = new SendUserMessage();
|
||||||
sendUserMessage.setMessage(message);
|
sendUserMessage.setMessage(message);
|
||||||
sendUserMessage.setType('ban'); //todo: is the type correct?
|
sendUserMessage.setType("ban"); //todo: is the type correct?
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
serverToClientMessage.setSendusermessage(sendUserMessage);
|
serverToClientMessage.setSendusermessage(sendUserMessage);
|
||||||
@ -686,13 +704,21 @@ export class SocketManager {
|
|||||||
public banUser(roomId: string, recipientUuid: string, message: string): void {
|
public banUser(roomId: string, recipientUuid: string, message: string): void {
|
||||||
const room = this.rooms.get(roomId);
|
const room = this.rooms.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In banUser, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In banUser, could not find room with id '" +
|
||||||
|
roomId +
|
||||||
|
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipient = room.getUserByUuid(recipientUuid);
|
const recipient = room.getUserByUuid(recipientUuid);
|
||||||
if (recipient === undefined) {
|
if (recipient === undefined) {
|
||||||
console.error("In banUser, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In banUser, could not find user with id '" +
|
||||||
|
recipientUuid +
|
||||||
|
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -701,7 +727,7 @@ export class SocketManager {
|
|||||||
|
|
||||||
const banUserMessage = new BanUserMessage();
|
const banUserMessage = new BanUserMessage();
|
||||||
banUserMessage.setMessage(message);
|
banUserMessage.setMessage(message);
|
||||||
banUserMessage.setType('banned');
|
banUserMessage.setType("banned");
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
serverToClientMessage.setBanusermessage(banUserMessage);
|
serverToClientMessage.setBanusermessage(banUserMessage);
|
||||||
@ -711,19 +737,22 @@ export class SocketManager {
|
|||||||
recipient.socket.end();
|
recipient.socket.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sendAdminRoomMessage(roomId: string, message: string) {
|
sendAdminRoomMessage(roomId: string, message: string) {
|
||||||
const room = this.rooms.get(roomId);
|
const room = this.rooms.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
//todo: this should cause the http call to return a 500
|
//todo: this should cause the http call to return a 500
|
||||||
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In sendAdminRoomMessage, could not find room with id '" +
|
||||||
|
roomId +
|
||||||
|
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
room.getUsers().forEach((recipient) => {
|
room.getUsers().forEach((recipient) => {
|
||||||
const sendUserMessage = new SendUserMessage();
|
const sendUserMessage = new SendUserMessage();
|
||||||
sendUserMessage.setMessage(message);
|
sendUserMessage.setMessage(message);
|
||||||
sendUserMessage.setType('message');
|
sendUserMessage.setType("message");
|
||||||
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
const clientMessage = new ServerToClientMessage();
|
||||||
clientMessage.setSendusermessage(sendUserMessage);
|
clientMessage.setSendusermessage(sendUserMessage);
|
||||||
@ -732,14 +761,18 @@ export class SocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchWorlFullWarning(roomId: string,): void {
|
dispatchWorlFullWarning(roomId: string): void {
|
||||||
const room = this.rooms.get(roomId);
|
const room = this.rooms.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
//todo: this should cause the http call to return a 500
|
//todo: this should cause the http call to return a 500
|
||||||
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In sendAdminRoomMessage, could not find room with id '" +
|
||||||
|
roomId +
|
||||||
|
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
room.getUsers().forEach((recipient) => {
|
room.getUsers().forEach((recipient) => {
|
||||||
const worldFullMessage = new WorldFullWarningMessage();
|
const worldFullMessage = new WorldFullWarningMessage();
|
||||||
|
|
||||||
@ -750,17 +783,17 @@ export class SocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchRoomRefresh(roomId: string,): void {
|
dispatchRoomRefresh(roomId: string): void {
|
||||||
const room = this.rooms.get(roomId);
|
const room = this.rooms.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionNumber = room.incrementVersion();
|
const versionNumber = room.incrementVersion();
|
||||||
room.getUsers().forEach((recipient) => {
|
room.getUsers().forEach((recipient) => {
|
||||||
const worldFullMessage = new RefreshRoomMessage();
|
const worldFullMessage = new RefreshRoomMessage();
|
||||||
worldFullMessage.setRoomid(roomId)
|
worldFullMessage.setRoomid(roomId);
|
||||||
worldFullMessage.setVersionnumber(versionNumber)
|
worldFullMessage.setVersionnumber(versionNumber);
|
||||||
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
const clientMessage = new ServerToClientMessage();
|
||||||
clientMessage.setRefreshroommessage(worldFullMessage);
|
clientMessage.setRefreshroommessage(worldFullMessage);
|
||||||
|
BIN
front/dist/resources/objects/layout_modes.png
vendored
BIN
front/dist/resources/objects/layout_modes.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 297 B |
@ -64,8 +64,8 @@
|
|||||||
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
||||||
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
|
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch",
|
"svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch",
|
||||||
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"",
|
"svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"",
|
||||||
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
||||||
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import {enableCameraSceneVisibilityStore, gameOverlayVisibilityStore} from "../Stores/MediaStore";
|
import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore";
|
||||||
import CameraControls from "./CameraControls.svelte";
|
import CameraControls from "./CameraControls.svelte";
|
||||||
import MyCamera from "./MyCamera.svelte";
|
import MyCamera from "./MyCamera.svelte";
|
||||||
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
|
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
|
||||||
@ -21,10 +21,13 @@
|
|||||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||||
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
|
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
|
||||||
import ErrorDialog from "./UI/ErrorDialog.svelte";
|
import ErrorDialog from "./UI/ErrorDialog.svelte";
|
||||||
|
import VideoOverlay from "./Video/VideoOverlay.svelte";
|
||||||
|
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
|
||||||
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
|
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
|
||||||
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
|
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
|
||||||
|
|
||||||
export let game: Game;
|
export let game: Game;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -68,6 +71,7 @@
|
|||||||
-->
|
-->
|
||||||
{#if $gameOverlayVisibilityStore}
|
{#if $gameOverlayVisibilityStore}
|
||||||
<div>
|
<div>
|
||||||
|
<VideoOverlay></VideoOverlay>
|
||||||
<MyCamera></MyCamera>
|
<MyCamera></MyCamera>
|
||||||
<CameraControls></CameraControls>
|
<CameraControls></CameraControls>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,6 +7,11 @@
|
|||||||
import cinemaCloseImg from "./images/cinema-close.svg";
|
import cinemaCloseImg from "./images/cinema-close.svg";
|
||||||
import microphoneImg from "./images/microphone.svg";
|
import microphoneImg from "./images/microphone.svg";
|
||||||
import microphoneCloseImg from "./images/microphone-close.svg";
|
import microphoneCloseImg from "./images/microphone-close.svg";
|
||||||
|
import layoutPresentationImg from "./images/layout-presentation.svg";
|
||||||
|
import layoutChatImg from "./images/layout-chat.svg";
|
||||||
|
import {layoutModeStore} from "../Stores/StreamableCollectionStore";
|
||||||
|
import {LayoutMode} from "../WebRtc/LayoutManager";
|
||||||
|
import {peerStore} from "../Stores/PeerStore";
|
||||||
|
|
||||||
function screenSharingClick(): void {
|
function screenSharingClick(): void {
|
||||||
if ($requestedScreenSharingState === true) {
|
if ($requestedScreenSharingState === true) {
|
||||||
@ -32,10 +37,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchLayoutMode() {
|
||||||
|
if ($layoutModeStore === LayoutMode.Presentation) {
|
||||||
|
$layoutModeStore = LayoutMode.VideoChat;
|
||||||
|
} else {
|
||||||
|
$layoutModeStore = LayoutMode.Presentation;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="btn-cam-action">
|
<div class="btn-cam-action">
|
||||||
|
<div class="btn-layout" on:click={switchLayoutMode} class:hide={$peerStore.size === 0}>
|
||||||
|
{#if $layoutModeStore === LayoutMode.Presentation }
|
||||||
|
<img src={layoutPresentationImg} style="padding: 2px" alt="Switch to mosaic mode">
|
||||||
|
{:else}
|
||||||
|
<img src={layoutChatImg} style="padding: 2px" alt="Switch to presentation mode">
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
|
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
|
||||||
{#if $requestedScreenSharingState}
|
{#if $requestedScreenSharingState}
|
||||||
<img src={monitorImg} alt="Start screen sharing">
|
<img src={monitorImg} alt="Start screen sharing">
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="horizontal-sound-meter" class:active={display}>
|
<div class="horizontal-sound-meter" class:active={display}>
|
||||||
{#each [...Array(NB_BARS).keys()] as i}
|
{#each [...Array(NB_BARS).keys()] as i (i)}
|
||||||
<div style={color(i, volume)}></div>
|
<div style={color(i, volume)}></div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,8 +6,6 @@
|
|||||||
export let stream: MediaStream|null;
|
export let stream: MediaStream|null;
|
||||||
let volume = 0;
|
let volume = 0;
|
||||||
|
|
||||||
const NB_BARS = 5;
|
|
||||||
|
|
||||||
let timeout: ReturnType<typeof setTimeout>;
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
const soundMeter = new SoundMeter();
|
const soundMeter = new SoundMeter();
|
||||||
let display = false;
|
let display = false;
|
||||||
@ -23,7 +21,7 @@
|
|||||||
|
|
||||||
timeout = setInterval(() => {
|
timeout = setInterval(() => {
|
||||||
try{
|
try{
|
||||||
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
|
volume = soundMeter.getVolume();
|
||||||
//console.log(volume);
|
//console.log(volume);
|
||||||
}catch(err){
|
}catch(err){
|
||||||
|
|
||||||
@ -45,9 +43,9 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="sound-progress" class:active={display}>
|
<div class="sound-progress" class:active={display}>
|
||||||
<span class:active={volume > 1}></span>
|
|
||||||
<span class:active={volume > 2}></span>
|
|
||||||
<span class:active={volume > 3}></span>
|
|
||||||
<span class:active={volume > 4}></span>
|
|
||||||
<span class:active={volume > 5}></span>
|
<span class:active={volume > 5}></span>
|
||||||
|
<span class:active={volume > 10}></span>
|
||||||
|
<span class:active={volume > 15}></span>
|
||||||
|
<span class:active={volume > 40}></span>
|
||||||
|
<span class:active={volume > 70}></span>
|
||||||
</div>
|
</div>
|
||||||
|
35
front/src/Components/Video/ChatLayout.svelte
Normal file
35
front/src/Components/Video/ChatLayout.svelte
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
|
||||||
|
import {afterUpdate, onDestroy} from "svelte";
|
||||||
|
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||||
|
import MediaBox from "./MediaBox.svelte";
|
||||||
|
|
||||||
|
let cssClass = 'one-col';
|
||||||
|
|
||||||
|
const unsubscribe = streamableCollectionStore.subscribe((displayableMedias) => {
|
||||||
|
const nbUsers = displayableMedias.size;
|
||||||
|
if (nbUsers <= 1) {
|
||||||
|
cssClass = 'one-col';
|
||||||
|
} else if (nbUsers <= 4) {
|
||||||
|
cssClass = 'two-col';
|
||||||
|
} else if (nbUsers <= 9) {
|
||||||
|
cssClass = 'three-col';
|
||||||
|
} else {
|
||||||
|
cssClass = 'four-col';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
biggestAvailableAreaStore.recompute();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chat-mode {cssClass}">
|
||||||
|
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
|
||||||
|
<MediaBox streamable={peer}></MediaBox>
|
||||||
|
{/each}
|
||||||
|
</div>
|
16
front/src/Components/Video/LocalStreamMediaBox.svelte
Normal file
16
front/src/Components/Video/LocalStreamMediaBox.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script lang="typescript">
|
||||||
|
import type {ScreenSharingLocalMedia} from "../../Stores/ScreenSharingStore";
|
||||||
|
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||||
|
import {srcObject} from "./utils";
|
||||||
|
|
||||||
|
export let peer : ScreenSharingLocalMedia;
|
||||||
|
let stream = peer.stream;
|
||||||
|
export let cssClass : string|undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="video-container {cssClass ? cssClass : ''}" class:hide={!stream}>
|
||||||
|
{#if stream}
|
||||||
|
<video use:srcObject={stream} autoplay muted playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||||
|
{/if}
|
||||||
|
</div>
|
20
front/src/Components/Video/MediaBox.svelte
Normal file
20
front/src/Components/Video/MediaBox.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {VideoPeer} from "../../WebRtc/VideoPeer";
|
||||||
|
import VideoMediaBox from "./VideoMediaBox.svelte";
|
||||||
|
import ScreenSharingMediaBox from "./ScreenSharingMediaBox.svelte";
|
||||||
|
import {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
|
||||||
|
import LocalStreamMediaBox from "./LocalStreamMediaBox.svelte";
|
||||||
|
import type {Streamable} from "../../Stores/StreamableCollectionStore";
|
||||||
|
|
||||||
|
export let streamable: Streamable;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="media-container">
|
||||||
|
{#if streamable instanceof VideoPeer}
|
||||||
|
<VideoMediaBox peer={streamable}/>
|
||||||
|
{:else if streamable instanceof ScreenSharingPeer}
|
||||||
|
<ScreenSharingMediaBox peer={streamable}/>
|
||||||
|
{:else}
|
||||||
|
<LocalStreamMediaBox peer={streamable} cssClass=""/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
24
front/src/Components/Video/PresentationLayout.svelte
Normal file
24
front/src/Components/Video/PresentationLayout.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {streamableCollectionStore} from "../../Stores/StreamableCollectionStore";
|
||||||
|
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||||
|
import {afterUpdate} from "svelte";
|
||||||
|
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||||
|
import MediaBox from "./MediaBox.svelte";
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
biggestAvailableAreaStore.recompute();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="main-section">
|
||||||
|
{#if $videoFocusStore }
|
||||||
|
<MediaBox streamable={$videoFocusStore}></MediaBox>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<aside class="sidebar">
|
||||||
|
{#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)}
|
||||||
|
{#if peer !== $videoFocusStore }
|
||||||
|
<MediaBox streamable={peer}></MediaBox>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</aside>
|
33
front/src/Components/Video/ScreenSharingMediaBox.svelte
Normal file
33
front/src/Components/Video/ScreenSharingMediaBox.svelte
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {ScreenSharingPeer} from "../../WebRtc/ScreenSharingPeer";
|
||||||
|
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||||
|
import {getColorByString, srcObject} from "./utils";
|
||||||
|
|
||||||
|
export let peer: ScreenSharingPeer;
|
||||||
|
let streamStore = peer.streamStore;
|
||||||
|
let name = peer.userName;
|
||||||
|
let statusStore = peer.statusStore;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="video-container">
|
||||||
|
{#if $statusStore === 'connecting'}
|
||||||
|
<div class="connecting-spinner"></div>
|
||||||
|
{/if}
|
||||||
|
{#if $statusStore === 'error'}
|
||||||
|
<div class="rtc-error"></div>
|
||||||
|
{/if}
|
||||||
|
{#if $streamStore === null}
|
||||||
|
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||||
|
{:else}
|
||||||
|
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.video-container {
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
48
front/src/Components/Video/VideoMediaBox.svelte
Normal file
48
front/src/Components/Video/VideoMediaBox.svelte
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {VideoPeer} from "../../WebRtc/VideoPeer";
|
||||||
|
import SoundMeterWidget from "../SoundMeterWidget.svelte";
|
||||||
|
import microphoneCloseImg from "../images/microphone-close.svg";
|
||||||
|
import reportImg from "./images/report.svg";
|
||||||
|
import blockSignImg from "./images/blockSign.svg";
|
||||||
|
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||||
|
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
|
||||||
|
import {getColorByString, srcObject} from "./utils";
|
||||||
|
|
||||||
|
export let peer: VideoPeer;
|
||||||
|
let streamStore = peer.streamStore;
|
||||||
|
let name = peer.userName;
|
||||||
|
let statusStore = peer.statusStore;
|
||||||
|
let constraintStore = peer.constraintsStore;
|
||||||
|
|
||||||
|
function openReport(peer: VideoPeer): void {
|
||||||
|
showReportScreenStore.set({ userId:peer.userId, userName: peer.userName });
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="video-container">
|
||||||
|
{#if $statusStore === 'connecting'}
|
||||||
|
<div class="connecting-spinner"></div>
|
||||||
|
{/if}
|
||||||
|
{#if $statusStore === 'error'}
|
||||||
|
<div class="rtc-error"></div>
|
||||||
|
{/if}
|
||||||
|
{#if !$constraintStore || $constraintStore.video === false}
|
||||||
|
<i style="background-color: {getColorByString(name)};">{name}</i>
|
||||||
|
{/if}
|
||||||
|
{#if $constraintStore && $constraintStore.audio === false}
|
||||||
|
<img src={microphoneCloseImg} alt="Muted">
|
||||||
|
{/if}
|
||||||
|
<button class="report" on:click={() => openReport(peer)}>
|
||||||
|
<img alt="Report this user" src={reportImg}>
|
||||||
|
<span>Report/Block</span>
|
||||||
|
</button>
|
||||||
|
{#if $streamStore }
|
||||||
|
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
|
||||||
|
{/if}
|
||||||
|
<img src={blockSignImg} class="block-logo" alt="Block" />
|
||||||
|
{#if $constraintStore && $constraintStore.audio !== false}
|
||||||
|
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
23
front/src/Components/Video/VideoOverlay.svelte
Normal file
23
front/src/Components/Video/VideoOverlay.svelte
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {LayoutMode} from "../../WebRtc/LayoutManager";
|
||||||
|
import {layoutModeStore} from "../../Stores/StreamableCollectionStore";
|
||||||
|
import PresentationLayout from "./PresentationLayout.svelte";
|
||||||
|
import ChatLayout from "./ChatLayout.svelte";
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="video-overlay">
|
||||||
|
{#if $layoutModeStore === LayoutMode.Presentation }
|
||||||
|
<PresentationLayout />
|
||||||
|
{:else }
|
||||||
|
<ChatLayout />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.video-overlay {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
22
front/src/Components/Video/images/blockSign.svg
Normal file
22
front/src/Components/Video/images/blockSign.svg
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 |
1
front/src/Components/Video/images/report.svg
Normal file
1
front/src/Components/Video/images/report.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.1 KiB |
27
front/src/Components/Video/utils.ts
Normal file
27
front/src/Components/Video/utils.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export function getColorByString(str: string) : string|null {
|
||||||
|
let hash = 0;
|
||||||
|
if (str.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
hash = hash & hash;
|
||||||
|
}
|
||||||
|
let color = '#';
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 255;
|
||||||
|
color += ('00' + value.toString(16)).substr(-2);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
||||||
|
node.srcObject = stream;
|
||||||
|
return {
|
||||||
|
update(newStream: MediaStream) {
|
||||||
|
if (node.srcObject != newStream) {
|
||||||
|
node.srcObject = newStream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
front/src/Components/images/layout-chat.svg
Normal file
1
front/src/Components/images/layout-chat.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48.97 39.04"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="0.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M35.08,11.78v8.75H24.31V11.78H35.08m1-1H23.31V21.53H36.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="0.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M34.87,26.05V34.8H24.11V26.05H34.87m1-1H23.11V35.8H35.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="0.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M34.87,40.07v8.75H24.11V40.07H34.87m1-1H23.11V49.82H35.87V39.07Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M53.08,11.78v8.75H42.31V11.78H53.08m1-1H41.31V21.53H54.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M52.87,26.05V34.8H42.11V26.05H52.87m1-1H41.11V35.8H53.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="18.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M52.87,40.07v8.75H42.11V40.07H52.87m1-1H41.11V49.82H53.87V39.07Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.7" y="0.5" width="11.76" height="9.75"/><path class="cls-1" d="M71.08,11.78v8.75H60.31V11.78H71.08m1-1H59.31V21.53H72.08V10.78Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.5" y="14.77" width="11.76" height="9.75"/><path class="cls-1" d="M70.87,26.05V34.8H60.11V26.05H70.87m1-1H59.11V35.8H71.87V25.05Z" transform="translate(-23.11 -10.78)"/><rect class="cls-1" x="36.5" y="28.79" width="11.76" height="9.75"/><path class="cls-1" d="M70.87,40.07v8.75H60.11V40.07H70.87m1-1H59.11V49.82H71.87V39.07Z" transform="translate(-23.11 -10.78)"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
front/src/Components/images/layout-presentation.svg
Normal file
1
front/src/Components/images/layout-presentation.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.83 54"><defs><style>.cls-1{fill:#fff;}</style></defs><rect class="cls-1" x="0.5" y="0.5" width="63.13" height="53"/><path class="cls-1" d="M67.12,6V58H5V6H67.12m1-1H4V59H68.12V5Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="0.75" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,6.25V18.12H73.37V6.25H87.83m1-1H72.37V19.12H88.83V5.25Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="17.69" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,23.19V35.05H73.37V23.19H87.83m1-1H72.37V36.05H88.83V22.19Z" transform="translate(-4 -5)"/><rect class="cls-1" x="68.87" y="34.75" width="15.46" height="12.86"/><path class="cls-1" d="M87.83,40.25V52.12H73.37V40.25H87.83m1-1H72.37V53.12H88.83V39.25Z" transform="translate(-4 -5)"/></svg>
|
After Width: | Height: | Size: 873 B |
@ -27,7 +27,6 @@ import {
|
|||||||
import { TextureError } from "../../Exception/TextureError";
|
import { TextureError } from "../../Exception/TextureError";
|
||||||
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
|
import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
|
||||||
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
|
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
|
||||||
import { peerStore } from "../../Stores/PeerStore";
|
|
||||||
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
import { touchScreenManager } from "../../Touch/TouchScreenManager";
|
||||||
import { urlManager } from "../../Url/UrlManager";
|
import { urlManager } from "../../Url/UrlManager";
|
||||||
import { audioManager } from "../../WebRtc/AudioManager";
|
import { audioManager } from "../../WebRtc/AudioManager";
|
||||||
@ -35,10 +34,10 @@ import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
|
|||||||
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
|
||||||
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
|
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
|
||||||
import {
|
import {
|
||||||
AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, CenterListener,
|
AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY,
|
||||||
|
Box,
|
||||||
JITSI_MESSAGE_PROPERTIES,
|
JITSI_MESSAGE_PROPERTIES,
|
||||||
layoutManager,
|
layoutManager,
|
||||||
LayoutMode,
|
|
||||||
ON_ACTION_TRIGGER_BUTTON,
|
ON_ACTION_TRIGGER_BUTTON,
|
||||||
TRIGGER_JITSI_PROPERTIES,
|
TRIGGER_JITSI_PROPERTIES,
|
||||||
TRIGGER_WEBSITE_PROPERTIES,
|
TRIGGER_WEBSITE_PROPERTIES,
|
||||||
@ -94,6 +93,9 @@ import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent';
|
|||||||
|
|
||||||
import AnimatedTiles from "phaser-animated-tiles";
|
import AnimatedTiles from "phaser-animated-tiles";
|
||||||
import {soundManager} from "./SoundManager";
|
import {soundManager} from "./SoundManager";
|
||||||
|
import {peerStore, screenSharingPeerStore} from "../../Stores/PeerStore";
|
||||||
|
import {videoFocusStore} from "../../Stores/VideoFocusStore";
|
||||||
|
import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore";
|
||||||
|
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
initPosition: PointInterface | null,
|
initPosition: PointInterface | null,
|
||||||
@ -132,7 +134,7 @@ interface DeleteGroupEventInterface {
|
|||||||
|
|
||||||
const defaultStartLayerName = 'start';
|
const defaultStartLayerName = 'start';
|
||||||
|
|
||||||
export class GameScene extends DirtyScene implements CenterListener {
|
export class GameScene extends DirtyScene {
|
||||||
Terrains: Array<Phaser.Tilemaps.Tileset>;
|
Terrains: Array<Phaser.Tilemaps.Tileset>;
|
||||||
CurrentPlayer!: Player;
|
CurrentPlayer!: Player;
|
||||||
MapPlayers!: Phaser.Physics.Arcade.Group;
|
MapPlayers!: Phaser.Physics.Arcade.Group;
|
||||||
@ -172,8 +174,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
y: -1000
|
y: -1000
|
||||||
}
|
}
|
||||||
|
|
||||||
private presentationModeSprite!: Sprite;
|
|
||||||
private chatModeSprite!: Sprite;
|
|
||||||
private gameMap!: GameMap;
|
private gameMap!: GameMap;
|
||||||
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.
|
||||||
@ -277,7 +277,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
this.onMapLoad(data);
|
this.onMapLoad(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(this.load as any).rexWebFont({
|
(this.load as any).rexWebFont({
|
||||||
@ -497,10 +496,6 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
this.outlinedItem?.activate();
|
this.outlinedItem?.activate();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.presentationModeSprite = new PresentationModeIcon(this, 36, this.game.renderer.height - 2);
|
|
||||||
this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this));
|
|
||||||
this.chatModeSprite = new ChatModeIcon(this, 70, this.game.renderer.height - 2);
|
|
||||||
this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this));
|
|
||||||
this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 2)
|
this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 2)
|
||||||
|
|
||||||
// FIXME: change this to use the UserInputManager class for input
|
// FIXME: change this to use the UserInputManager class for input
|
||||||
@ -512,7 +507,8 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
this.reposition();
|
this.reposition();
|
||||||
|
|
||||||
// From now, this game scene will be notified of reposition events
|
// From now, this game scene will be notified of reposition events
|
||||||
layoutManager.setListener(this);
|
biggestAvailableAreaStore.subscribe((box) => this.updateCameraOffset(box));
|
||||||
|
|
||||||
this.triggerOnMapLayerPropertyChange();
|
this.triggerOnMapLayerPropertyChange();
|
||||||
this.listenToIframeEvents();
|
this.listenToIframeEvents();
|
||||||
|
|
||||||
@ -643,21 +639,19 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
// When connection is performed, let's connect SimplePeer
|
// When connection is performed, let's connect SimplePeer
|
||||||
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
|
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
|
||||||
peerStore.connectToSimplePeer(this.simplePeer);
|
peerStore.connectToSimplePeer(this.simplePeer);
|
||||||
|
screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
|
||||||
|
videoFocusStore.connectToSimplePeer(this.simplePeer);
|
||||||
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
|
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
|
||||||
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
|
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
this.simplePeer.registerPeerConnectionListener({
|
this.simplePeer.registerPeerConnectionListener({
|
||||||
onConnect(user: UserSimplePeerInterface) {
|
onConnect(peer) {
|
||||||
self.presentationModeSprite.setVisible(true);
|
|
||||||
self.chatModeSprite.setVisible(true);
|
|
||||||
self.openChatIcon.setVisible(true);
|
self.openChatIcon.setVisible(true);
|
||||||
audioManager.decreaseVolume();
|
audioManager.decreaseVolume();
|
||||||
},
|
},
|
||||||
onDisconnect(userId: number) {
|
onDisconnect(userId: number) {
|
||||||
if (self.simplePeer.getNbConnections() === 0) {
|
if (self.simplePeer.getNbConnections() === 0) {
|
||||||
self.presentationModeSprite.setVisible(false);
|
|
||||||
self.chatModeSprite.setVisible(false);
|
|
||||||
self.openChatIcon.setVisible(false);
|
self.openChatIcon.setVisible(false);
|
||||||
audioManager.restoreVolume();
|
audioManager.restoreVolume();
|
||||||
}
|
}
|
||||||
@ -1058,23 +1052,6 @@ ${escapedMessage}
|
|||||||
this.MapPlayersByKey = new Map<number, RemotePlayer>();
|
this.MapPlayersByKey = new Map<number, RemotePlayer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private switchLayoutMode(): void {
|
|
||||||
//if discussion is activated, this layout cannot be activated
|
|
||||||
if (mediaManager.activatedDiscussion) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const mode = layoutManager.getLayoutMode();
|
|
||||||
if (mode === LayoutMode.Presentation) {
|
|
||||||
layoutManager.switchLayoutMode(LayoutMode.VideoChat);
|
|
||||||
this.presentationModeSprite.setFrame(1);
|
|
||||||
this.chatModeSprite.setFrame(2);
|
|
||||||
} else {
|
|
||||||
layoutManager.switchLayoutMode(LayoutMode.Presentation);
|
|
||||||
this.presentationModeSprite.setFrame(0);
|
|
||||||
this.chatModeSprite.setFrame(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private initStartXAndStartY() {
|
private initStartXAndStartY() {
|
||||||
// If there is an init position passed
|
// If there is an init position passed
|
||||||
if (this.initPosition !== null) {
|
if (this.initPosition !== null) {
|
||||||
@ -1187,7 +1164,7 @@ ${escapedMessage}
|
|||||||
initCamera() {
|
initCamera() {
|
||||||
this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
|
this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
|
||||||
this.cameras.main.startFollow(this.CurrentPlayer, true);
|
this.cameras.main.startFollow(this.CurrentPlayer, true);
|
||||||
this.updateCameraOffset();
|
biggestAvailableAreaStore.recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
createCollisionWithPlayer() {
|
createCollisionWithPlayer() {
|
||||||
@ -1334,7 +1311,7 @@ ${escapedMessage}
|
|||||||
* @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
|
* @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
|
||||||
*/
|
*/
|
||||||
update(time: number, delta: number): void {
|
update(time: number, delta: number): void {
|
||||||
mediaManager.updateScene();
|
this.dirty = false;
|
||||||
this.currentTick = time;
|
this.currentTick = time;
|
||||||
this.CurrentPlayer.moveUser(delta);
|
this.CurrentPlayer.moveUser(delta);
|
||||||
|
|
||||||
@ -1564,20 +1541,17 @@ ${escapedMessage}
|
|||||||
|
|
||||||
}
|
}
|
||||||
private reposition(): void {
|
private reposition(): void {
|
||||||
this.presentationModeSprite.setY(this.game.renderer.height - 2);
|
|
||||||
this.chatModeSprite.setY(this.game.renderer.height - 2);
|
|
||||||
this.openChatIcon.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();
|
biggestAvailableAreaStore.recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the offset of the character compared to the center of the screen according to the layout manager
|
* Updates the offset of the character compared to the center of the screen according to the layout manager
|
||||||
* (tries to put the character in the center of the remaining space if there is a discussion going on.
|
* (tries to put the character in the center of the remaining space if there is a discussion going on.
|
||||||
*/
|
*/
|
||||||
private updateCameraOffset(): void {
|
private updateCameraOffset(array: Box): void {
|
||||||
const array = layoutManager.findBiggestAvailableArray();
|
|
||||||
const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart;
|
const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart;
|
||||||
const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart;
|
const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart;
|
||||||
|
|
||||||
@ -1587,10 +1561,6 @@ ${escapedMessage}
|
|||||||
this.cameras.main.setFollowOffset((xCenter - game.offsetWidth / 2) * window.devicePixelRatio / this.scale.zoom, (yCenter - game.offsetHeight / 2) * window.devicePixelRatio / this.scale.zoom);
|
this.cameras.main.setFollowOffset((xCenter - game.offsetWidth / 2) * window.devicePixelRatio / this.scale.zoom, (yCenter - game.offsetHeight / 2) * window.devicePixelRatio / this.scale.zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCenterChange(): void {
|
|
||||||
this.updateCameraOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
public startJitsi(roomName: string, jwt?: string): void {
|
public startJitsi(roomName: string, jwt?: string): void {
|
||||||
const allProps = this.gameMap.getCurrentProperties();
|
const allProps = this.gameMap.getCurrentProperties();
|
||||||
const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string | undefined, 'jitsiConfig');
|
const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string | undefined, 'jitsiConfig');
|
||||||
@ -1650,6 +1620,6 @@ ${escapedMessage}
|
|||||||
|
|
||||||
zoomByFactor(zoomFactor: number) {
|
zoomByFactor(zoomFactor: number) {
|
||||||
waScaleManager.zoomModifier *= zoomFactor;
|
waScaleManager.zoomModifier *= zoomFactor;
|
||||||
this.updateCameraOffset();
|
biggestAvailableAreaStore.recompute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,17 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha
|
|||||||
import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene";
|
import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene";
|
||||||
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 {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu";
|
import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu";
|
||||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
import {connectionManager} from "../../Connexion/ConnectionManager";
|
||||||
import {GameConnexionTypes} from "../../Url/UrlManager";
|
import {GameConnexionTypes} from "../../Url/UrlManager";
|
||||||
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
|
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
|
||||||
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
|
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
|
||||||
import {menuIconVisible} from "../../Stores/MenuStore";
|
import {menuIconVisible} from "../../Stores/MenuStore";
|
||||||
|
import {videoConstraintStore} from "../../Stores/MediaStore";
|
||||||
|
import {showReportScreenStore} from "../../Stores/ShowReportScreenStore";
|
||||||
import { HtmlUtils } from '../../WebRtc/HtmlUtils';
|
import { HtmlUtils } from '../../WebRtc/HtmlUtils';
|
||||||
import { iframeListener } from '../../Api/IframeListener';
|
import { iframeListener } from '../../Api/IframeListener';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { videoConstraintStore } from "../../Stores/MediaStore";
|
|
||||||
import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent";
|
import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent";
|
||||||
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
|
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
|
||||||
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
|
import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore";
|
||||||
@ -111,9 +111,11 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
|
this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous);
|
||||||
mediaManager.setShowReportModalCallBacks((userId, userName) => {
|
showReportScreenStore.subscribe((user) => {
|
||||||
this.closeAll();
|
this.closeAll();
|
||||||
this.gameReportElement.open(parseInt(userId), userName);
|
if (user !== null) {
|
||||||
|
this.gameReportElement.open(user.userId, user.userName);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.input.keyboard.on('keyup-TAB', () => {
|
this.input.keyboard.on('keyup-TAB', () => {
|
||||||
|
127
front/src/Stores/BiggestAvailableAreaStore.ts
Normal file
127
front/src/Stores/BiggestAvailableAreaStore.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import {get, writable} from "svelte/store";
|
||||||
|
import type {Box} from "../WebRtc/LayoutManager";
|
||||||
|
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
||||||
|
import {LayoutMode} from "../WebRtc/LayoutManager";
|
||||||
|
import {layoutModeStore} from "./StreamableCollectionStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
|
||||||
|
*/
|
||||||
|
function findBiggestAvailableArea(): Box {
|
||||||
|
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas');
|
||||||
|
if (get(layoutModeStore) === LayoutMode.VideoChat) {
|
||||||
|
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
|
||||||
|
const htmlChildren = Array.from(children.values());
|
||||||
|
|
||||||
|
// No chat? Let's go full center
|
||||||
|
if (htmlChildren.length === 0) {
|
||||||
|
return {
|
||||||
|
xStart: 0,
|
||||||
|
yStart: 0,
|
||||||
|
xEnd: game.offsetWidth,
|
||||||
|
yEnd: game.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastDiv = htmlChildren[htmlChildren.length - 1];
|
||||||
|
// Compute area between top right of the last div and bottom right of window
|
||||||
|
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth))
|
||||||
|
* (game.offsetHeight - lastDiv.offsetTop);
|
||||||
|
|
||||||
|
// Compute area between bottom of last div and bottom of the screen on whole width
|
||||||
|
const area2 = game.offsetWidth
|
||||||
|
* (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
|
||||||
|
|
||||||
|
if (area1 < 0 && area2 < 0) {
|
||||||
|
// If screen is full, let's not attempt something foolish and simply center character in the middle.
|
||||||
|
return {
|
||||||
|
xStart: 0,
|
||||||
|
yStart: 0,
|
||||||
|
xEnd: game.offsetWidth,
|
||||||
|
yEnd: game.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (area1 <= area2) {
|
||||||
|
return {
|
||||||
|
xStart: 0,
|
||||||
|
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
||||||
|
xEnd: game.offsetWidth,
|
||||||
|
yEnd: game.offsetHeight
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
||||||
|
yStart: lastDiv.offsetTop,
|
||||||
|
xEnd: game.offsetWidth,
|
||||||
|
yEnd: game.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Possible destinations: at the center bottom or at the right bottom.
|
||||||
|
const mainSectionChildren = Array.from(document.querySelectorAll<HTMLDivElement>('div.main-section > div').values());
|
||||||
|
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('aside.sidebar > div').values());
|
||||||
|
|
||||||
|
// No presentation? Let's center on the screen
|
||||||
|
if (mainSectionChildren.length === 0) {
|
||||||
|
return {
|
||||||
|
xStart: 0,
|
||||||
|
yStart: 0,
|
||||||
|
xEnd: game.offsetWidth,
|
||||||
|
yEnd: game.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we know we have at least one element in the main section.
|
||||||
|
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1];
|
||||||
|
|
||||||
|
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight))
|
||||||
|
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
||||||
|
|
||||||
|
let leftSideBar: number;
|
||||||
|
let bottomSideBar: number;
|
||||||
|
if (sidebarChildren.length === 0) {
|
||||||
|
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').offsetLeft;
|
||||||
|
bottomSideBar = 0;
|
||||||
|
} else {
|
||||||
|
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
|
||||||
|
leftSideBar = lastSideBarChildren.offsetLeft;
|
||||||
|
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
||||||
|
}
|
||||||
|
const sideBarArea = (game.offsetWidth - leftSideBar)
|
||||||
|
* (game.offsetHeight - bottomSideBar);
|
||||||
|
|
||||||
|
if (presentationArea <= sideBarArea) {
|
||||||
|
return {
|
||||||
|
xStart: leftSideBar,
|
||||||
|
yStart: bottomSideBar,
|
||||||
|
xEnd: game.offsetWidth,
|
||||||
|
yEnd: game.offsetHeight
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
xStart: 0,
|
||||||
|
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
|
||||||
|
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
|
||||||
|
yEnd: game.offsetHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the list of (video) peers we are connected to.
|
||||||
|
*/
|
||||||
|
function createBiggestAvailableAreaStore() {
|
||||||
|
|
||||||
|
const { subscribe, set } = writable<Box>({xStart:0, yStart: 0, xEnd: 1, yEnd: 1});
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
recompute: () => {
|
||||||
|
set(findBiggestAvailableArea());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const biggestAvailableAreaStore = createBiggestAvailableAreaStore();
|
17
front/src/Stores/GameOverlayStoreVisibility.ts
Normal file
17
front/src/Stores/GameOverlayStoreVisibility.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {writable} from "svelte/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains whether the game overlay is shown or not.
|
||||||
|
* Typically, the overlay is hidden when entering Jitsi meet.
|
||||||
|
*/
|
||||||
|
function createGameOverlayVisibilityStore() {
|
||||||
|
const { subscribe, set, update } = writable(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
showGameOverlay: () => set(true),
|
||||||
|
hideGameOverlay: () => set(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore();
|
@ -1,13 +1,14 @@
|
|||||||
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
|
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
|
||||||
import {peerStore} from "./PeerStore";
|
|
||||||
import {localUserStore} from "../Connexion/LocalUserStore";
|
import {localUserStore} from "../Connexion/LocalUserStore";
|
||||||
import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap";
|
|
||||||
import {userMovingStore} from "./GameStore";
|
import {userMovingStore} from "./GameStore";
|
||||||
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
||||||
import {BrowserTooOldError} from "./Errors/BrowserTooOldError";
|
import {BrowserTooOldError} from "./Errors/BrowserTooOldError";
|
||||||
import {errorStore} from "./ErrorStore";
|
import {errorStore} from "./ErrorStore";
|
||||||
import {isIOS} from "../WebRtc/DeviceUtils";
|
import {isIOS} from "../WebRtc/DeviceUtils";
|
||||||
import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS";
|
import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS";
|
||||||
|
import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility";
|
||||||
|
import {peerStore} from "./PeerStore";
|
||||||
|
import {privacyShutdownStore} from "./PrivacyShutdownStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains the camera state requested by the user (on or off).
|
* A store that contains the camera state requested by the user (on or off).
|
||||||
@ -35,35 +36,6 @@ function createRequestedMicrophoneState() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A store containing whether the current page is visible or not.
|
|
||||||
*/
|
|
||||||
export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) {
|
|
||||||
const onVisibilityChange = () => {
|
|
||||||
set(document.visibilityState === 'visible');
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
||||||
|
|
||||||
return function stop() {
|
|
||||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A store that contains whether the game overlay is shown or not.
|
|
||||||
* Typically, the overlay is hidden when entering Jitsi meet.
|
|
||||||
*/
|
|
||||||
function createGameOverlayVisibilityStore() {
|
|
||||||
const { subscribe, set, update } = writable(false);
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
showGameOverlay: () => set(true),
|
|
||||||
hideGameOverlay: () => set(false),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains whether the EnableCameraScene is shown or not.
|
* A store that contains whether the EnableCameraScene is shown or not.
|
||||||
*/
|
*/
|
||||||
@ -79,44 +51,8 @@ function createEnableCameraSceneVisibilityStore() {
|
|||||||
|
|
||||||
export const requestedCameraState = createRequestedCameraState();
|
export const requestedCameraState = createRequestedCameraState();
|
||||||
export const requestedMicrophoneState = createRequestedMicrophoneState();
|
export const requestedMicrophoneState = createRequestedMicrophoneState();
|
||||||
export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore();
|
|
||||||
export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore();
|
export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore();
|
||||||
|
|
||||||
/**
|
|
||||||
* A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion.
|
|
||||||
*/
|
|
||||||
function createPrivacyShutdownStore() {
|
|
||||||
let privacyEnabled = false;
|
|
||||||
|
|
||||||
const { subscribe, set, update } = writable(privacyEnabled);
|
|
||||||
|
|
||||||
visibilityStore.subscribe((isVisible) => {
|
|
||||||
if (!isVisible && get(peerStore).size === 0) {
|
|
||||||
privacyEnabled = true;
|
|
||||||
set(true);
|
|
||||||
}
|
|
||||||
if (isVisible) {
|
|
||||||
privacyEnabled = false;
|
|
||||||
set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
peerStore.subscribe((peers) => {
|
|
||||||
if (peers.size === 0 && get(visibilityStore) === false) {
|
|
||||||
privacyEnabled = true;
|
|
||||||
set(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const privacyShutdownStore = createPrivacyShutdownStore();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store containing whether the webcam was enabled in the last 10 seconds
|
* A store containing whether the webcam was enabled in the last 10 seconds
|
||||||
*/
|
*/
|
||||||
|
@ -1,26 +1,62 @@
|
|||||||
import { derived, writable, Writable } from "svelte/store";
|
import {readable, writable} from "svelte/store";
|
||||||
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
|
import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer";
|
||||||
import type {SimplePeer} from "../WebRtc/SimplePeer";
|
import {VideoPeer} from "../WebRtc/VideoPeer";
|
||||||
|
import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store that contains the camera state requested by the user (on or off).
|
* A store that contains the list of (video) peers we are connected to.
|
||||||
*/
|
*/
|
||||||
function createPeerStore() {
|
function createPeerStore() {
|
||||||
let users = new Map<number, UserSimplePeerInterface>();
|
let peers = new Map<number, VideoPeer>();
|
||||||
|
|
||||||
const { subscribe, set, update } = writable(users);
|
const { subscribe, set, update } = writable(peers);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||||
users = new Map<number, UserSimplePeerInterface>();
|
peers = new Map<number, VideoPeer>();
|
||||||
set(users);
|
set(peers);
|
||||||
simplePeer.registerPeerConnectionListener({
|
simplePeer.registerPeerConnectionListener({
|
||||||
onConnect(user: UserSimplePeerInterface) {
|
onConnect(peer: RemotePeer) {
|
||||||
|
if (peer instanceof VideoPeer) {
|
||||||
|
update(users => {
|
||||||
|
users.set(peer.userId, peer);
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDisconnect(userId: number) {
|
||||||
update(users => {
|
update(users => {
|
||||||
users.set(user.userId, user);
|
users.delete(userId);
|
||||||
return users;
|
return users;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the list of screen sharing peers we are connected to.
|
||||||
|
*/
|
||||||
|
function createScreenSharingPeerStore() {
|
||||||
|
let peers = new Map<number, ScreenSharingPeer>();
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable(peers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||||
|
peers = new Map<number, ScreenSharingPeer>();
|
||||||
|
set(peers);
|
||||||
|
simplePeer.registerPeerConnectionListener({
|
||||||
|
onConnect(peer: RemotePeer) {
|
||||||
|
if (peer instanceof ScreenSharingPeer) {
|
||||||
|
update(users => {
|
||||||
|
users.set(peer.userId, peer);
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDisconnect(userId: number) {
|
onDisconnect(userId: number) {
|
||||||
update(users => {
|
update(users => {
|
||||||
@ -34,3 +70,56 @@ function createPeerStore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const peerStore = createPeerStore();
|
export const peerStore = createPeerStore();
|
||||||
|
export const screenSharingPeerStore = createScreenSharingPeerStore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains ScreenSharingPeer, ONLY if those ScreenSharingPeer are emitting a stream towards us!
|
||||||
|
*/
|
||||||
|
function createScreenSharingStreamStore() {
|
||||||
|
let peers = new Map<number, ScreenSharingPeer>();
|
||||||
|
|
||||||
|
return readable<Map<number, ScreenSharingPeer>>(peers, function start(set) {
|
||||||
|
|
||||||
|
let unsubscribes: (()=>void)[] = [];
|
||||||
|
|
||||||
|
const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => {
|
||||||
|
for (const unsubscribe of unsubscribes) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
unsubscribes = [];
|
||||||
|
|
||||||
|
peers = new Map<number, ScreenSharingPeer>();
|
||||||
|
|
||||||
|
screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => {
|
||||||
|
|
||||||
|
if (screenSharingPeer.isReceivingScreenSharingStream()) {
|
||||||
|
peers.set(key, screenSharingPeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribes.push(screenSharingPeer.streamStore.subscribe((stream) => {
|
||||||
|
if (stream) {
|
||||||
|
peers.set(key, screenSharingPeer);
|
||||||
|
} else {
|
||||||
|
peers.delete(key);
|
||||||
|
}
|
||||||
|
set(peers);
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
set(peers);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return function stop() {
|
||||||
|
unsubscribe();
|
||||||
|
for (const unsubscribe of unsubscribes) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const screenSharingStreamStore = createScreenSharingStreamStore();
|
||||||
|
|
||||||
|
|
||||||
|
37
front/src/Stores/PrivacyShutdownStore.ts
Normal file
37
front/src/Stores/PrivacyShutdownStore.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {get, writable} from "svelte/store";
|
||||||
|
import {peerStore} from "./PeerStore";
|
||||||
|
import {visibilityStore} from "./VisibilityStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion.
|
||||||
|
*/
|
||||||
|
function createPrivacyShutdownStore() {
|
||||||
|
let privacyEnabled = false;
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable(privacyEnabled);
|
||||||
|
|
||||||
|
visibilityStore.subscribe((isVisible) => {
|
||||||
|
if (!isVisible && get(peerStore).size === 0) {
|
||||||
|
privacyEnabled = true;
|
||||||
|
set(true);
|
||||||
|
}
|
||||||
|
if (isVisible) {
|
||||||
|
privacyEnabled = false;
|
||||||
|
set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
peerStore.subscribe((peers) => {
|
||||||
|
if (peers.size === 0 && get(visibilityStore) === false) {
|
||||||
|
privacyEnabled = true;
|
||||||
|
set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const privacyShutdownStore = createPrivacyShutdownStore();
|
@ -1,16 +1,10 @@
|
|||||||
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
|
import {derived, get, Readable, readable, writable, Writable} from "svelte/store";
|
||||||
import {peerStore} from "./PeerStore";
|
import {peerStore} from "./PeerStore";
|
||||||
import {localUserStore} from "../Connexion/LocalUserStore";
|
import type {
|
||||||
import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap";
|
LocalStreamStoreValue,
|
||||||
import {userMovingStore} from "./GameStore";
|
|
||||||
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
|
||||||
import {
|
|
||||||
audioConstraintStore, cameraEnergySavingStore,
|
|
||||||
enableCameraSceneVisibilityStore,
|
|
||||||
gameOverlayVisibilityStore, LocalStreamStoreValue, privacyShutdownStore,
|
|
||||||
requestedCameraState,
|
|
||||||
requestedMicrophoneState, videoConstraintStore
|
|
||||||
} from "./MediaStore";
|
} from "./MediaStore";
|
||||||
|
import {DivImportance} from "../WebRtc/LayoutManager";
|
||||||
|
import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility";
|
||||||
|
|
||||||
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
|
||||||
@ -191,3 +185,33 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set)
|
|||||||
|
|
||||||
set($peerStore.size !== 0);
|
set($peerStore.size !== 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface ScreenSharingLocalMedia {
|
||||||
|
uniqueId: string;
|
||||||
|
stream: MediaStream|null;
|
||||||
|
//subscribe(this: void, run: Subscriber<ScreenSharingLocalMedia>, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The representation of the screen sharing stream.
|
||||||
|
*/
|
||||||
|
export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia|null>(null, function start(set) {
|
||||||
|
|
||||||
|
const localMedia: ScreenSharingLocalMedia = {
|
||||||
|
uniqueId: "localScreenSharingStream",
|
||||||
|
stream: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => {
|
||||||
|
if (screenSharingLocalStream.type === "success") {
|
||||||
|
localMedia.stream = screenSharingLocalStream.stream;
|
||||||
|
} else {
|
||||||
|
localMedia.stream = null;
|
||||||
|
}
|
||||||
|
set(localMedia);
|
||||||
|
});
|
||||||
|
|
||||||
|
return function stop() {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
})
|
||||||
|
3
front/src/Stores/ShowReportScreenStore.ts
Normal file
3
front/src/Stores/ShowReportScreenStore.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import {writable} from "svelte/store";
|
||||||
|
|
||||||
|
export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null);
|
43
front/src/Stores/StreamableCollectionStore.ts
Normal file
43
front/src/Stores/StreamableCollectionStore.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {derived, get, Readable, writable} from "svelte/store";
|
||||||
|
import {ScreenSharingLocalMedia, screenSharingLocalMedia} from "./ScreenSharingStore";
|
||||||
|
import { peerStore, screenSharingStreamStore} from "./PeerStore";
|
||||||
|
import type {RemotePeer} from "../WebRtc/SimplePeer";
|
||||||
|
import {LayoutMode} from "../WebRtc/LayoutManager";
|
||||||
|
|
||||||
|
export type Streamable = RemotePeer | ScreenSharingLocalMedia;
|
||||||
|
|
||||||
|
export const layoutModeStore = writable<LayoutMode>(LayoutMode.Presentation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains everything that can produce a stream (so the peers + the local screen sharing stream)
|
||||||
|
*/
|
||||||
|
function createStreamableCollectionStore(): Readable<Map<string, Streamable>> {
|
||||||
|
|
||||||
|
return derived([
|
||||||
|
screenSharingStreamStore,
|
||||||
|
peerStore,
|
||||||
|
screenSharingLocalMedia,
|
||||||
|
], ([
|
||||||
|
$screenSharingStreamStore,
|
||||||
|
$peerStore,
|
||||||
|
$screenSharingLocalMedia,
|
||||||
|
], set) => {
|
||||||
|
|
||||||
|
const peers = new Map<string, Streamable>();
|
||||||
|
|
||||||
|
const addPeer = (peer: Streamable) => {
|
||||||
|
peers.set(peer.uniqueId, peer);
|
||||||
|
};
|
||||||
|
|
||||||
|
$screenSharingStreamStore.forEach(addPeer);
|
||||||
|
$peerStore.forEach(addPeer);
|
||||||
|
|
||||||
|
if ($screenSharingLocalMedia?.stream) {
|
||||||
|
addPeer($screenSharingLocalMedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(peers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const streamableCollectionStore = createStreamableCollectionStore();
|
47
front/src/Stores/VideoFocusStore.ts
Normal file
47
front/src/Stores/VideoFocusStore.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {writable} from "svelte/store";
|
||||||
|
import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer";
|
||||||
|
import {VideoPeer} from "../WebRtc/VideoPeer";
|
||||||
|
import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer";
|
||||||
|
import type {Streamable} from "./StreamableCollectionStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the peer / media that has currently the "importance" focus.
|
||||||
|
*/
|
||||||
|
function createVideoFocusStore() {
|
||||||
|
const { subscribe, set, update } = writable<Streamable | null>(null);
|
||||||
|
|
||||||
|
let focusedMedia: Streamable | null = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
focus: (media: Streamable) => {
|
||||||
|
focusedMedia = media;
|
||||||
|
set(media);
|
||||||
|
},
|
||||||
|
removeFocus: () => {
|
||||||
|
focusedMedia = null;
|
||||||
|
set(null);
|
||||||
|
},
|
||||||
|
toggleFocus: (media: Streamable) => {
|
||||||
|
if (media !== focusedMedia) {
|
||||||
|
focusedMedia = media;
|
||||||
|
} else {
|
||||||
|
focusedMedia = null;
|
||||||
|
}
|
||||||
|
set(focusedMedia);
|
||||||
|
},
|
||||||
|
connectToSimplePeer: (simplePeer: SimplePeer) => {
|
||||||
|
simplePeer.registerPeerConnectionListener({
|
||||||
|
onConnect(peer: RemotePeer) {
|
||||||
|
},
|
||||||
|
onDisconnect(userId: number) {
|
||||||
|
if ((focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) && focusedMedia.userId === userId) {
|
||||||
|
set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const videoFocusStore = createVideoFocusStore();
|
16
front/src/Stores/VisibilityStore.ts
Normal file
16
front/src/Stores/VisibilityStore.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {readable} from "svelte/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store containing whether the current page is visible or not.
|
||||||
|
*/
|
||||||
|
export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) {
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
set(document.visibilityState === 'visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
|
||||||
|
return function stop() {
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
};
|
||||||
|
});
|
@ -1,9 +1,9 @@
|
|||||||
import {HtmlUtils} from "./HtmlUtils";
|
import {HtmlUtils} from "./HtmlUtils";
|
||||||
import type {ShowReportCallBack} from "./MediaManager";
|
|
||||||
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
|
import type {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";
|
||||||
import {iframeListener} from "../Api/IframeListener";
|
import {iframeListener} from "../Api/IframeListener";
|
||||||
|
import {showReportScreenStore} from "../Stores/ShowReportScreenStore";
|
||||||
|
|
||||||
export type SendMessageCallback = (message:string) => void;
|
export type SendMessageCallback = (message:string) => void;
|
||||||
|
|
||||||
@ -104,11 +104,10 @@ export class DiscussionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addParticipant(
|
public addParticipant(
|
||||||
userId: number|string,
|
userId: number|'me',
|
||||||
name: string|undefined,
|
name: string|undefined,
|
||||||
img?: string|undefined,
|
img?: string|undefined,
|
||||||
isMe: boolean = false,
|
isMe: boolean = false,
|
||||||
showReportCallBack?: ShowReportCallBack
|
|
||||||
) {
|
) {
|
||||||
const divParticipant: HTMLDivElement = document.createElement('div');
|
const divParticipant: HTMLDivElement = document.createElement('div');
|
||||||
divParticipant.classList.add('participant');
|
divParticipant.classList.add('participant');
|
||||||
@ -132,16 +131,13 @@ export class DiscussionManager {
|
|||||||
!isMe
|
!isMe
|
||||||
&& connectionManager.getConnexionType
|
&& connectionManager.getConnexionType
|
||||||
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous
|
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous
|
||||||
|
&& userId !== 'me'
|
||||||
) {
|
) {
|
||||||
const reportBanUserAction: HTMLButtonElement = document.createElement('button');
|
const reportBanUserAction: HTMLButtonElement = document.createElement('button');
|
||||||
reportBanUserAction.classList.add('report-btn')
|
reportBanUserAction.classList.add('report-btn')
|
||||||
reportBanUserAction.innerText = 'Report';
|
reportBanUserAction.innerText = 'Report';
|
||||||
reportBanUserAction.addEventListener('click', () => {
|
reportBanUserAction.addEventListener('click', () => {
|
||||||
if(showReportCallBack) {
|
showReportScreenStore.set({ userId: userId, userName: name ? name : ''});
|
||||||
showReportCallBack(`${userId}`, name);
|
|
||||||
}else{
|
|
||||||
console.info('report feature is not activated!');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
divParticipant.appendChild(reportBanUserAction);
|
divParticipant.appendChild(reportBanUserAction);
|
||||||
}
|
}
|
||||||
|
@ -15,14 +15,6 @@ export enum DivImportance {
|
|||||||
Normal = "Normal",
|
Normal = "Normal",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Classes implementing this interface can be notified when the center of the screen (the player position) should be
|
|
||||||
* changed.
|
|
||||||
*/
|
|
||||||
export interface CenterListener {
|
|
||||||
onCenterChange(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ON_ACTION_TRIGGER_BUTTON = 'onaction';
|
export const ON_ACTION_TRIGGER_BUTTON = 'onaction';
|
||||||
|
|
||||||
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger';
|
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger';
|
||||||
@ -34,293 +26,12 @@ export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage';
|
|||||||
export const AUDIO_VOLUME_PROPERTY = 'audioVolume';
|
export const AUDIO_VOLUME_PROPERTY = 'audioVolume';
|
||||||
export const AUDIO_LOOP_PROPERTY = 'audioLoop';
|
export const AUDIO_LOOP_PROPERTY = 'audioLoop';
|
||||||
|
|
||||||
/**
|
export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number};
|
||||||
* This class is in charge of the video-conference layout.
|
|
||||||
* It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode.
|
|
||||||
*/
|
|
||||||
class LayoutManager {
|
class LayoutManager {
|
||||||
private mode: LayoutMode = LayoutMode.Presentation;
|
|
||||||
|
|
||||||
private importantDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
|
||||||
private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
|
||||||
private listener: CenterListener|null = null;
|
|
||||||
|
|
||||||
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
|
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
|
||||||
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
||||||
|
|
||||||
public setListener(centerListener: CenterListener|null) {
|
|
||||||
this.listener = centerListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public add(importance: DivImportance, userId: string, html: string): void {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = html;
|
|
||||||
div.id = "user-"+userId;
|
|
||||||
div.className = "media-container"
|
|
||||||
div.onclick = () => {
|
|
||||||
const parentId = div.parentElement?.id;
|
|
||||||
if (parentId === 'sidebar' || parentId === 'chat-mode') {
|
|
||||||
this.focusOn(userId);
|
|
||||||
} else {
|
|
||||||
this.removeFocusOn(userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (importance === DivImportance.Important) {
|
|
||||||
this.importantDivs.set(userId, div);
|
|
||||||
|
|
||||||
// If this is the first video with high importance, let's switch mode automatically.
|
|
||||||
if (this.importantDivs.size === 1 && this.mode === LayoutMode.VideoChat) {
|
|
||||||
this.switchLayoutMode(LayoutMode.Presentation);
|
|
||||||
}
|
|
||||||
} else if (importance === DivImportance.Normal) {
|
|
||||||
this.normalDivs.set(userId, div);
|
|
||||||
} else {
|
|
||||||
throw new Error('Unexpected importance');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.positionDiv(div, importance);
|
|
||||||
this.adjustVideoChatClass();
|
|
||||||
this.listener?.onCenterChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
private positionDiv(elem: HTMLDivElement, importance: DivImportance): void {
|
|
||||||
if (this.mode === LayoutMode.VideoChat) {
|
|
||||||
const chatModeDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode');
|
|
||||||
chatModeDiv.appendChild(elem);
|
|
||||||
} else {
|
|
||||||
if (importance === DivImportance.Important) {
|
|
||||||
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section');
|
|
||||||
mainSectionDiv.appendChild(elem);
|
|
||||||
} else if (importance === DivImportance.Normal) {
|
|
||||||
const sideBarDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar');
|
|
||||||
sideBarDiv.appendChild(elem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Put the screen in presentation mode and move elem in presentation mode (and all other videos in normal mode)
|
|
||||||
*/
|
|
||||||
private focusOn(userId: string): void {
|
|
||||||
const focusedDiv = this.getDivByUserId(userId);
|
|
||||||
for (const [importantUserId, importantDiv] of this.importantDivs.entries()) {
|
|
||||||
//this.positionDiv(importantDiv, DivImportance.Normal);
|
|
||||||
this.importantDivs.delete(importantUserId);
|
|
||||||
this.normalDivs.set(importantUserId, importantDiv);
|
|
||||||
}
|
|
||||||
this.normalDivs.delete(userId);
|
|
||||||
this.importantDivs.set(userId, focusedDiv);
|
|
||||||
//this.positionDiv(focusedDiv, DivImportance.Important);
|
|
||||||
this.switchLayoutMode(LayoutMode.Presentation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes userId from presentation mode
|
|
||||||
*/
|
|
||||||
private removeFocusOn(userId: string): void {
|
|
||||||
const importantDiv = this.importantDivs.get(userId);
|
|
||||||
if (importantDiv === undefined) {
|
|
||||||
throw new Error('Div with user id "'+userId+'" is not in important mode');
|
|
||||||
}
|
|
||||||
this.normalDivs.set(userId, importantDiv);
|
|
||||||
this.importantDivs.delete(userId);
|
|
||||||
|
|
||||||
this.positionDiv(importantDiv, DivImportance.Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDivByUserId(userId: string): HTMLDivElement {
|
|
||||||
let div = this.importantDivs.get(userId);
|
|
||||||
if (div !== undefined) {
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
div = this.normalDivs.get(userId);
|
|
||||||
if (div !== undefined) {
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
throw new Error('Could not find media with user id '+userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the DIV matching userId.
|
|
||||||
*/
|
|
||||||
public remove(userId: string): void {
|
|
||||||
console.log('Removing video for userID '+userId+'.');
|
|
||||||
let div = this.importantDivs.get(userId);
|
|
||||||
if (div !== undefined) {
|
|
||||||
div.remove();
|
|
||||||
this.importantDivs.delete(userId);
|
|
||||||
this.adjustVideoChatClass();
|
|
||||||
this.listener?.onCenterChange();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
div = this.normalDivs.get(userId);
|
|
||||||
if (div !== undefined) {
|
|
||||||
div.remove();
|
|
||||||
this.normalDivs.delete(userId);
|
|
||||||
this.adjustVideoChatClass();
|
|
||||||
this.listener?.onCenterChange();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Cannot remove userID '+userId+'. Already removed?');
|
|
||||||
//throw new Error('Could not find user ID "'+userId+'"');
|
|
||||||
}
|
|
||||||
|
|
||||||
private adjustVideoChatClass(): void {
|
|
||||||
const chatModeDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode');
|
|
||||||
chatModeDiv.classList.remove('one-col', 'two-col', 'three-col', 'four-col');
|
|
||||||
|
|
||||||
const nbUsers = this.importantDivs.size + this.normalDivs.size;
|
|
||||||
|
|
||||||
if (nbUsers <= 1) {
|
|
||||||
chatModeDiv.classList.add('one-col');
|
|
||||||
} else if (nbUsers <= 4) {
|
|
||||||
chatModeDiv.classList.add('two-col');
|
|
||||||
} else if (nbUsers <= 9) {
|
|
||||||
chatModeDiv.classList.add('three-col');
|
|
||||||
} else {
|
|
||||||
chatModeDiv.classList.add('four-col');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public switchLayoutMode(layoutMode: LayoutMode) {
|
|
||||||
this.mode = layoutMode;
|
|
||||||
|
|
||||||
if (layoutMode === LayoutMode.Presentation) {
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'flex';
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'flex';
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'none';
|
|
||||||
} else {
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'none';
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'none';
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'grid';
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const div of this.importantDivs.values()) {
|
|
||||||
this.positionDiv(div, DivImportance.Important);
|
|
||||||
}
|
|
||||||
for (const div of this.normalDivs.values()) {
|
|
||||||
this.positionDiv(div, DivImportance.Normal);
|
|
||||||
}
|
|
||||||
this.listener?.onCenterChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLayoutMode(): LayoutMode {
|
|
||||||
return this.mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*public getGameCenter(): {x: number, y: number} {
|
|
||||||
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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} {
|
|
||||||
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas');
|
|
||||||
if (this.mode === LayoutMode.VideoChat) {
|
|
||||||
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
|
|
||||||
const htmlChildren = Array.from(children.values());
|
|
||||||
|
|
||||||
// No chat? Let's go full center
|
|
||||||
if (htmlChildren.length === 0) {
|
|
||||||
return {
|
|
||||||
xStart: 0,
|
|
||||||
yStart: 0,
|
|
||||||
xEnd: game.offsetWidth,
|
|
||||||
yEnd: game.offsetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastDiv = htmlChildren[htmlChildren.length - 1];
|
|
||||||
// Compute area between top right of the last div and bottom right of window
|
|
||||||
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth))
|
|
||||||
* (game.offsetHeight - lastDiv.offsetTop);
|
|
||||||
|
|
||||||
// Compute area between bottom of last div and bottom of the screen on whole width
|
|
||||||
const area2 = game.offsetWidth
|
|
||||||
* (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
|
|
||||||
|
|
||||||
if (area1 < 0 && area2 < 0) {
|
|
||||||
// If screen is full, let's not attempt something foolish and simply center character in the middle.
|
|
||||||
return {
|
|
||||||
xStart: 0,
|
|
||||||
yStart: 0,
|
|
||||||
xEnd: game.offsetWidth,
|
|
||||||
yEnd: game.offsetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (area1 <= area2) {
|
|
||||||
console.log('lastDiv', lastDiv.offsetTop, lastDiv.offsetHeight);
|
|
||||||
return {
|
|
||||||
xStart: 0,
|
|
||||||
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
|
||||||
xEnd: game.offsetWidth,
|
|
||||||
yEnd: game.offsetHeight
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('lastDiv', lastDiv.offsetTop);
|
|
||||||
return {
|
|
||||||
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
|
||||||
yStart: lastDiv.offsetTop,
|
|
||||||
xEnd: game.offsetWidth,
|
|
||||||
yEnd: game.offsetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Possible destinations: at the center bottom or at the right bottom.
|
|
||||||
const mainSectionChildren = Array.from(document.querySelectorAll<HTMLDivElement>('div.main-section > div').values());
|
|
||||||
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('aside.sidebar > div').values());
|
|
||||||
|
|
||||||
// No presentation? Let's center on the screen
|
|
||||||
if (mainSectionChildren.length === 0) {
|
|
||||||
return {
|
|
||||||
xStart: 0,
|
|
||||||
yStart: 0,
|
|
||||||
xEnd: game.offsetWidth,
|
|
||||||
yEnd: game.offsetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, we know we have at least one element in the main section.
|
|
||||||
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1];
|
|
||||||
|
|
||||||
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight))
|
|
||||||
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
|
||||||
|
|
||||||
let leftSideBar: number;
|
|
||||||
let bottomSideBar: number;
|
|
||||||
if (sidebarChildren.length === 0) {
|
|
||||||
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').offsetLeft;
|
|
||||||
bottomSideBar = 0;
|
|
||||||
} else {
|
|
||||||
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
|
|
||||||
leftSideBar = lastSideBarChildren.offsetLeft;
|
|
||||||
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
|
||||||
}
|
|
||||||
const sideBarArea = (game.offsetWidth - leftSideBar)
|
|
||||||
* (game.offsetHeight - bottomSideBar);
|
|
||||||
|
|
||||||
if (presentationArea <= sideBarArea) {
|
|
||||||
return {
|
|
||||||
xStart: leftSideBar,
|
|
||||||
yStart: bottomSideBar,
|
|
||||||
xEnd: game.offsetWidth,
|
|
||||||
yEnd: game.offsetHeight
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
xStart: 0,
|
|
||||||
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
|
|
||||||
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
|
|
||||||
yEnd: game.offsetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){
|
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){
|
||||||
//delete previous element
|
//delete previous element
|
||||||
this.removeActionButton(id, userInputManager);
|
this.removeActionButton(id, userInputManager);
|
||||||
|
@ -7,7 +7,7 @@ import type { UserSimplePeerInterface } from "./SimplePeer";
|
|||||||
import { SoundMeter } from "../Phaser/Components/SoundMeter";
|
import { SoundMeter } from "../Phaser/Components/SoundMeter";
|
||||||
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
|
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
|
||||||
import {
|
import {
|
||||||
gameOverlayVisibilityStore, localStreamStore,
|
localStreamStore,
|
||||||
} from "../Stores/MediaStore";
|
} from "../Stores/MediaStore";
|
||||||
import {
|
import {
|
||||||
screenSharingLocalStreamStore
|
screenSharingLocalStreamStore
|
||||||
@ -17,20 +17,13 @@ import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore"
|
|||||||
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
|
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 ShowReportCallBack = (userId: string, userName: string | undefined) => void;
|
|
||||||
export type HelpCameraSettingsCallBack = () => void;
|
|
||||||
|
|
||||||
import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
|
import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
|
||||||
|
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
|
||||||
|
|
||||||
export class MediaManager {
|
export class MediaManager {
|
||||||
private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
|
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
||||||
//mySoundMeterElement: HTMLDivElement;
|
|
||||||
|
|
||||||
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
|
|
||||||
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
|
|
||||||
showReportModalCallBacks: Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -40,21 +33,8 @@ export class MediaManager {
|
|||||||
|
|
||||||
private userInputManager?: UserInputManager;
|
private userInputManager?: UserInputManager;
|
||||||
|
|
||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
|
||||||
/*private mySoundMeter?: SoundMeter|null;
|
|
||||||
private soundMeters: Map<string, SoundMeter> = new Map<string, SoundMeter>();
|
|
||||||
private soundMeterElements: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();*/
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
||||||
this.pingCameraStatus();
|
|
||||||
|
|
||||||
//FIX ME SOUNDMETER: check stability of sound meter calculation
|
|
||||||
/*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter'));
|
|
||||||
this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => {
|
|
||||||
this.mySoundMeterElement.children.item(index)?.classList.remove('active');
|
|
||||||
});*/
|
|
||||||
|
|
||||||
//Check of ask notification navigator permission
|
//Check of ask notification navigator permission
|
||||||
this.getNotification();
|
this.getNotification();
|
||||||
|
|
||||||
@ -68,7 +48,6 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let isScreenSharing = false;
|
|
||||||
screenSharingLocalStreamStore.subscribe((result) => {
|
screenSharingLocalStreamStore.subscribe((result) => {
|
||||||
if (result.type === 'error') {
|
if (result.type === 'error') {
|
||||||
console.error(result.error);
|
console.error(result.error);
|
||||||
@ -77,32 +56,7 @@ export class MediaManager {
|
|||||||
}, this.userInputManager);
|
}, this.userInputManager);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.stream !== null) {
|
|
||||||
isScreenSharing = true;
|
|
||||||
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
|
|
||||||
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = result.stream;
|
|
||||||
} else {
|
|
||||||
if (isScreenSharing) {
|
|
||||||
isScreenSharing = false;
|
|
||||||
this.removeActiveScreenSharingVideo('me');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*screenSharingAvailableStore.subscribe((available) => {
|
|
||||||
if (available) {
|
|
||||||
document.querySelector('.btn-monitor')?.classList.remove('hide');
|
|
||||||
} else {
|
|
||||||
document.querySelector('.btn-monitor')?.classList.add('hide');
|
|
||||||
}
|
|
||||||
});*/
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateScene(){
|
|
||||||
//FIX ME SOUNDMETER: check stability of sound meter calculation
|
|
||||||
//this.updateSoudMeter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public showGameOverlay(): void {
|
public showGameOverlay(): void {
|
||||||
@ -137,71 +91,6 @@ export class MediaManager {
|
|||||||
gameOverlayVisibilityStore.hideGameOverlay();
|
gameOverlayVisibilityStore.hideGameOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
addActiveVideo(user: UserSimplePeerInterface, userName: string = "") {
|
|
||||||
|
|
||||||
const userId = '' + user.userId
|
|
||||||
|
|
||||||
userName = userName.toUpperCase();
|
|
||||||
const color = this.getColorByString(userName);
|
|
||||||
|
|
||||||
const html = `
|
|
||||||
<div id="div-${userId}" class="video-container">
|
|
||||||
<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}" 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 playsinline></video>
|
|
||||||
<img src="resources/logos/blockSign.svg" id="blocking-${userId}" class="block-logo">
|
|
||||||
<div id="soundMeter-${userId}" class="sound-progress">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
layoutManager.add(DivImportance.Normal, userId, html);
|
|
||||||
|
|
||||||
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
|
|
||||||
|
|
||||||
//permit to create participant in discussion part
|
|
||||||
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){
|
|
||||||
|
|
||||||
userId = this.getScreenSharingId(userId);
|
|
||||||
const html = `
|
|
||||||
<div id="div-${userId}" class="video-container">
|
|
||||||
<video id="${userId}" autoplay playsinline></video>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
layoutManager.add(divImportance, userId, html);
|
|
||||||
|
|
||||||
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private getScreenSharingId(userId: string): string {
|
private getScreenSharingId(userId: string): string {
|
||||||
return `screen-sharing-${userId}`;
|
return `screen-sharing-${userId}`;
|
||||||
}
|
}
|
||||||
@ -248,61 +137,6 @@ export class MediaManager {
|
|||||||
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-' + userId);
|
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-' + userId);
|
||||||
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
|
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
|
||||||
}
|
}
|
||||||
addStreamRemoteVideo(userId: string, stream: MediaStream): void {
|
|
||||||
const remoteVideo = this.remoteVideo.get(userId);
|
|
||||||
if (remoteVideo === undefined) {
|
|
||||||
throw `Unable to find video for ${userId}`;
|
|
||||||
}
|
|
||||||
remoteVideo.srcObject = stream;
|
|
||||||
|
|
||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
|
||||||
//sound metter
|
|
||||||
/*const soundMeter = new SoundMeter();
|
|
||||||
soundMeter.connectToSource(stream, new AudioContext());
|
|
||||||
this.soundMeters.set(userId, soundMeter);
|
|
||||||
this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail<HTMLImageElement>('soundMeter-'+userId));*/
|
|
||||||
}
|
|
||||||
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(this.getScreenSharingId(userId));
|
|
||||||
if (remoteVideo === undefined) {
|
|
||||||
this.addScreenSharingActiveVideo(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeActiveVideo(userId: string) {
|
|
||||||
layoutManager.remove(userId);
|
|
||||||
this.remoteVideo.delete(userId);
|
|
||||||
|
|
||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
|
||||||
/*this.soundMeters.get(userId)?.stop();
|
|
||||||
this.soundMeters.delete(userId);
|
|
||||||
this.soundMeterElements.delete(userId);*/
|
|
||||||
|
|
||||||
//permit to remove user in discussion part
|
|
||||||
this.removeParticipant(userId);
|
|
||||||
}
|
|
||||||
removeActiveScreenSharingVideo(userId: string) {
|
|
||||||
this.removeActiveVideo(this.getScreenSharingId(userId))
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnecting(userId: string): void {
|
|
||||||
const connectingSpinnerDiv = this.getSpinner(userId);
|
|
||||||
if (connectingSpinnerDiv === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connectingSpinnerDiv.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnected(userId: string): void {
|
|
||||||
const connectingSpinnerDiv = this.getSpinner(userId);
|
|
||||||
if (connectingSpinnerDiv === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
connectingSpinnerDiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
isError(userId: string): void {
|
isError(userId: string): void {
|
||||||
console.info("isError", `div-${userId}`);
|
console.info("isError", `div-${userId}`);
|
||||||
@ -326,33 +160,11 @@ export class MediaManager {
|
|||||||
if (!element) {
|
if (!element) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement | null;
|
const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null;
|
||||||
return connnectingSpinnerDiv;
|
return connectingSpinnerDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getColorByString(str: String): String | null {
|
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){
|
||||||
let hash = 0;
|
|
||||||
if (str.length === 0) return null;
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
||||||
hash = hash & hash;
|
|
||||||
}
|
|
||||||
let color = '#';
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const value = (hash >> (i * 8)) & 255;
|
|
||||||
color += ('00' + value.toString(16)).substr(-2);
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public addNewParticipant(userId: number | string, name: string | undefined, img?: string, showReportUserCallBack?: ShowReportCallBack) {
|
|
||||||
discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeParticipant(userId: number | string) {
|
|
||||||
discussionManager.removeParticipant(userId);
|
|
||||||
}
|
|
||||||
public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
|
|
||||||
this.triggerCloseJistiFrame.set(id, Function);
|
this.triggerCloseJistiFrame.set(id, Function);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,16 +177,6 @@ export class MediaManager {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* For some reasons, the microphone muted icon or the stream is not always up to date.
|
|
||||||
* Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser)
|
|
||||||
**/
|
|
||||||
private pingCameraStatus() {
|
|
||||||
/*setInterval(() => {
|
|
||||||
console.log('ping camera status');
|
|
||||||
this.triggerUpdatedLocalStreamCallbacks(this.localStream);
|
|
||||||
}, 30000);*/
|
|
||||||
}
|
|
||||||
|
|
||||||
public addNewMessage(name: string, message: string, isMe: boolean = false) {
|
public addNewMessage(name: string, message: string, isMe: boolean = false) {
|
||||||
discussionManager.addMessage(name, message, isMe);
|
discussionManager.addMessage(name, message, isMe);
|
||||||
@ -389,61 +191,11 @@ export class MediaManager {
|
|||||||
discussionManager.onSendMessageCallback(userId, callback);
|
discussionManager.onSendMessageCallback(userId, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activatedDiscussion() {
|
public setUserInputManager(userInputManager : UserInputManager){
|
||||||
return discussionManager.activatedDiscussion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setUserInputManager(userInputManager: UserInputManager) {
|
|
||||||
this.userInputManager = userInputManager;
|
this.userInputManager = userInputManager;
|
||||||
discussionManager.setUserInputManager(userInputManager);
|
discussionManager.setUserInputManager(userInputManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setShowReportModalCallBacks(callback: ShowReportCallBack) {
|
|
||||||
this.showReportModalCallBacks.add(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
|
||||||
/*updateSoudMeter(){
|
|
||||||
try{
|
|
||||||
const volume = parseInt(((this.mySoundMeter ? this.mySoundMeter.getVolume() : 0) / 10).toFixed(0));
|
|
||||||
this.setVolumeSoundMeter(volume, this.mySoundMeterElement);
|
|
||||||
|
|
||||||
for(const indexUserId of this.soundMeters.keys()){
|
|
||||||
const soundMeter = this.soundMeters.get(indexUserId);
|
|
||||||
const soundMeterElement = this.soundMeterElements.get(indexUserId);
|
|
||||||
if (!soundMeter || !soundMeterElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const volumeByUser = parseInt((soundMeter.getVolume() / 10).toFixed(0));
|
|
||||||
this.setVolumeSoundMeter(volumeByUser, soundMeterElement);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
//console.error(err);
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
private setVolumeSoundMeter(volume: number, element: HTMLDivElement) {
|
|
||||||
if (volume <= 0 && !element.classList.contains('active')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
element.classList.remove('active');
|
|
||||||
if (volume <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
element.classList.add('active');
|
|
||||||
element.childNodes.forEach((value: ChildNode, index) => {
|
|
||||||
const elementChildren = element.children.item(index);
|
|
||||||
if (!elementChildren) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
elementChildren.classList.remove('active');
|
|
||||||
if ((index + 1) > volume) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
elementChildren.classList.add('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNotification(){
|
public getNotification(){
|
||||||
//Get notification
|
//Get notification
|
||||||
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
|
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import type * as SimplePeerNamespace from "simple-peer";
|
import type * as SimplePeerNamespace from "simple-peer";
|
||||||
import {mediaManager} from "./MediaManager";
|
import {mediaManager} from "./MediaManager";
|
||||||
import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
|
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
|
||||||
import type {RoomConnection} from "../Connexion/RoomConnection";
|
import type {RoomConnection} from "../Connexion/RoomConnection";
|
||||||
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
|
import {MESSAGE_TYPE_CONSTRAINT, PeerStatus} from "./VideoPeer";
|
||||||
import type {UserSimplePeerInterface} from "./SimplePeer";
|
import type {UserSimplePeerInterface} from "./SimplePeer";
|
||||||
|
import {Readable, readable, writable, Writable} from "svelte/store";
|
||||||
|
import {videoFocusStore} from "../Stores/VideoFocusStore";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||||
|
|
||||||
@ -17,9 +19,12 @@ 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;
|
public readonly userId: number;
|
||||||
|
public readonly uniqueId: string;
|
||||||
|
public readonly streamStore: Readable<MediaStream | null>;
|
||||||
|
public readonly statusStore: Readable<PeerStatus>;
|
||||||
|
|
||||||
constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, stream: MediaStream | null) {
|
constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) {
|
||||||
super({
|
super({
|
||||||
initiator: initiator ? initiator : false,
|
initiator: initiator ? initiator : false,
|
||||||
//reconnectTimer: 10000,
|
//reconnectTimer: 10000,
|
||||||
@ -38,6 +43,55 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.userId = user.userId;
|
this.userId = user.userId;
|
||||||
|
this.uniqueId = 'screensharing_'+this.userId;
|
||||||
|
|
||||||
|
this.streamStore = readable<MediaStream|null>(null, (set) => {
|
||||||
|
const onStream = (stream: MediaStream|null) => {
|
||||||
|
videoFocusStore.focus(this);
|
||||||
|
set(stream);
|
||||||
|
};
|
||||||
|
const onData = (chunk: Buffer) => {
|
||||||
|
// We unfortunately need to rely on an event to let the other party know a stream has stopped.
|
||||||
|
// It seems there is no native way to detect that.
|
||||||
|
// TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event
|
||||||
|
const message = JSON.parse(chunk.toString('utf8'));
|
||||||
|
if (message.streamEnded !== true) {
|
||||||
|
console.error('Unexpected message on screen sharing peer connection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on('stream', onStream);
|
||||||
|
this.on('data', onData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.off('stream', onStream);
|
||||||
|
this.off('data', onData);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
||||||
|
const onConnect = () => {
|
||||||
|
set('connected');
|
||||||
|
};
|
||||||
|
const onError = () => {
|
||||||
|
set('error');
|
||||||
|
};
|
||||||
|
const onClose = () => {
|
||||||
|
set('closed');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.on('connect', onConnect);
|
||||||
|
this.on('error', onError);
|
||||||
|
this.on('close', onClose);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.off('connect', onConnect);
|
||||||
|
this.off('error', onError);
|
||||||
|
this.off('close', onClose);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
//start listen signal for the peer connection
|
//start listen signal for the peer connection
|
||||||
this.on('signal', (data: unknown) => {
|
this.on('signal', (data: unknown) => {
|
||||||
@ -54,27 +108,13 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
this.destroy();
|
this.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('data', (chunk: Buffer) => {
|
|
||||||
// We unfortunately need to rely on an event to let the other party know a stream has stopped.
|
|
||||||
// It seems there is no native way to detect that.
|
|
||||||
const message = JSON.parse(chunk.toString('utf8'));
|
|
||||||
if (message.streamEnded !== true) {
|
|
||||||
console.error('Unexpected message on screen sharing peer connection');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mediaManager.removeActiveScreenSharingVideo("" + this.userId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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(`screen sharing error => ${this.userId} => ${err.code}`, err);
|
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
|
||||||
//mediaManager.isErrorScreenSharing(this.userId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('connect', () => {
|
this.on('connect', () => {
|
||||||
this._connected = true;
|
this._connected = true;
|
||||||
// FIXME: we need to put the loader on the screen sharing connection
|
|
||||||
mediaManager.isConnected("" + this.userId);
|
|
||||||
console.info(`connect => ${this.userId}`);
|
console.info(`connect => ${this.userId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -88,7 +128,6 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sendWebrtcScreenSharingSignal(data: unknown) {
|
private sendWebrtcScreenSharingSignal(data: unknown) {
|
||||||
//console.log("sendWebrtcScreenSharingSignal", data);
|
|
||||||
try {
|
try {
|
||||||
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
|
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
|
||||||
}catch (e) {
|
}catch (e) {
|
||||||
@ -100,13 +139,9 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
* Sends received stream to screen.
|
* Sends received stream to screen.
|
||||||
*/
|
*/
|
||||||
private stream(stream?: MediaStream) {
|
private stream(stream?: MediaStream) {
|
||||||
//console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream);
|
|
||||||
//console.log(`stream => ${this.userId} => `, stream);
|
|
||||||
if(!stream){
|
if(!stream){
|
||||||
mediaManager.removeActiveScreenSharingVideo("" + this.userId);
|
|
||||||
this.isReceivingStream = false;
|
this.isReceivingStream = false;
|
||||||
} else {
|
} else {
|
||||||
mediaManager.addStreamRemoteScreenSharing("" + this.userId, stream);
|
|
||||||
this.isReceivingStream = true;
|
this.isReceivingStream = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,7 +156,6 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
if(!this.toClose){
|
if(!this.toClose){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mediaManager.removeActiveScreenSharingVideo("" + 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.
|
||||||
//console.log('Closing connection with '+userId);
|
//console.log('Closing connection with '+userId);
|
||||||
|
@ -6,19 +6,15 @@ import {
|
|||||||
mediaManager,
|
mediaManager,
|
||||||
StartScreenSharingCallback,
|
StartScreenSharingCallback,
|
||||||
StopScreenSharingCallback,
|
StopScreenSharingCallback,
|
||||||
UpdatedLocalStreamCallback
|
|
||||||
} from "./MediaManager";
|
} from "./MediaManager";
|
||||||
import {ScreenSharingPeer} from "./ScreenSharingPeer";
|
import {ScreenSharingPeer} from "./ScreenSharingPeer";
|
||||||
import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
|
import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
|
||||||
import type {RoomConnection} from "../Connexion/RoomConnection";
|
import type {RoomConnection} from "../Connexion/RoomConnection";
|
||||||
import {connectionManager} from "../Connexion/ConnectionManager";
|
|
||||||
import {GameConnexionTypes} from "../Url/UrlManager";
|
|
||||||
import {blackListManager} from "./BlackListManager";
|
import {blackListManager} from "./BlackListManager";
|
||||||
import {get} from "svelte/store";
|
import {get} from "svelte/store";
|
||||||
import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
||||||
import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
|
import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
|
||||||
import {DivImportance, layoutManager} from "./LayoutManager";
|
import {discussionManager} from "./DiscussionManager";
|
||||||
import {HtmlUtils} from "./HtmlUtils";
|
|
||||||
|
|
||||||
export interface UserSimplePeerInterface{
|
export interface UserSimplePeerInterface{
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -28,8 +24,10 @@ export interface UserSimplePeerInterface{
|
|||||||
webRtcPassword?: string|undefined;
|
webRtcPassword?: string|undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RemotePeer = VideoPeer | ScreenSharingPeer;
|
||||||
|
|
||||||
export interface PeerConnectionListener {
|
export interface PeerConnectionListener {
|
||||||
onConnect(user: UserSimplePeerInterface): void;
|
onConnect(user: RemotePeer): void;
|
||||||
|
|
||||||
onDisconnect(userId: number): void;
|
onDisconnect(userId: number): void;
|
||||||
}
|
}
|
||||||
@ -124,7 +122,6 @@ export class SimplePeer {
|
|||||||
// 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)
|
|
||||||
if(!user.initiator){
|
if(!user.initiator){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -159,20 +156,15 @@ export class SimplePeer {
|
|||||||
|
|
||||||
let name = user.name;
|
let name = user.name;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === user.userId);
|
name = this.getName(user.userId);
|
||||||
if (userSearch) {
|
|
||||||
name = userSearch.name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaManager.removeActiveVideo("" + user.userId);
|
discussionManager.removeParticipant(user.userId);
|
||||||
|
|
||||||
mediaManager.addActiveVideo(user, name);
|
|
||||||
|
|
||||||
this.lastWebrtcUserName = user.webRtcUser;
|
this.lastWebrtcUserName = user.webRtcUser;
|
||||||
this.lastWebrtcPassword = user.webRtcPassword;
|
this.lastWebrtcPassword = user.webRtcPassword;
|
||||||
|
|
||||||
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection, localStream);
|
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
|
||||||
|
|
||||||
//permit to send message
|
//permit to send message
|
||||||
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
|
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
|
||||||
@ -196,11 +188,20 @@ export class SimplePeer {
|
|||||||
this.PeerConnectionArray.set(user.userId, peer);
|
this.PeerConnectionArray.set(user.userId, peer);
|
||||||
|
|
||||||
for (const peerConnectionListener of this.peerConnectionListeners) {
|
for (const peerConnectionListener of this.peerConnectionListeners) {
|
||||||
peerConnectionListener.onConnect(user);
|
peerConnectionListener.onConnect(peer);
|
||||||
}
|
}
|
||||||
return peer;
|
return peer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getName(userId: number): string {
|
||||||
|
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId);
|
||||||
|
if (userSearch) {
|
||||||
|
return userSearch.name || '';
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create peer connection to bind users
|
* create peer connection to bind users
|
||||||
*/
|
*/
|
||||||
@ -221,23 +222,19 @@ export class SimplePeer {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should display the screen sharing ONLY if we are not initiator
|
|
||||||
if (!user.initiator) {
|
|
||||||
mediaManager.removeActiveScreenSharingVideo("" + user.userId);
|
|
||||||
mediaManager.addScreenSharingActiveVideo("" + user.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing)
|
// Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing)
|
||||||
if (user.webRtcUser === undefined) {
|
if (user.webRtcUser === undefined) {
|
||||||
user.webRtcUser = this.lastWebrtcUserName;
|
user.webRtcUser = this.lastWebrtcUserName;
|
||||||
user.webRtcPassword = this.lastWebrtcPassword;
|
user.webRtcPassword = this.lastWebrtcPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection, stream);
|
const name = this.getName(user.userId);
|
||||||
|
|
||||||
|
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream);
|
||||||
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
|
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
|
||||||
|
|
||||||
for (const peerConnectionListener of this.peerConnectionListeners) {
|
for (const peerConnectionListener of this.peerConnectionListeners) {
|
||||||
peerConnectionListener.onConnect(user);
|
peerConnectionListener.onConnect(peer);
|
||||||
}
|
}
|
||||||
return peer;
|
return peer;
|
||||||
}
|
}
|
||||||
@ -288,7 +285,7 @@ export class SimplePeer {
|
|||||||
*/
|
*/
|
||||||
private closeScreenSharingConnection(userId : number) {
|
private closeScreenSharingConnection(userId : number) {
|
||||||
try {
|
try {
|
||||||
mediaManager.removeActiveScreenSharingVideo("" + userId);
|
//mediaManager.removeActiveScreenSharingVideo("" + userId);
|
||||||
const peer = this.PeerScreenSharingConnectionArray.get(userId);
|
const peer = this.PeerScreenSharingConnectionArray.get(userId);
|
||||||
if (peer === undefined) {
|
if (peer === undefined) {
|
||||||
console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user")
|
console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user")
|
||||||
|
@ -5,11 +5,14 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
|
|||||||
import {blackListManager} from "./BlackListManager";
|
import {blackListManager} from "./BlackListManager";
|
||||||
import type {Subscription} from "rxjs";
|
import type {Subscription} from "rxjs";
|
||||||
import type {UserSimplePeerInterface} from "./SimplePeer";
|
import type {UserSimplePeerInterface} from "./SimplePeer";
|
||||||
import {get} from "svelte/store";
|
import {get, readable, Readable} from "svelte/store";
|
||||||
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
|
||||||
|
import {discussionManager} from "./DiscussionManager";
|
||||||
|
|
||||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||||
|
|
||||||
|
export type PeerStatus = "connecting" | "connected" | "error" | "closed";
|
||||||
|
|
||||||
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_BLOCKED = 'blocked';
|
||||||
@ -22,12 +25,15 @@ export class VideoPeer extends Peer {
|
|||||||
public _connected: boolean = false;
|
public _connected: boolean = false;
|
||||||
private remoteStream!: MediaStream;
|
private remoteStream!: MediaStream;
|
||||||
private blocked: boolean = false;
|
private blocked: boolean = false;
|
||||||
private userId: number;
|
public readonly userId: number;
|
||||||
private userName: string;
|
public readonly uniqueId: string;
|
||||||
private onBlockSubscribe: Subscription;
|
private onBlockSubscribe: Subscription;
|
||||||
private onUnBlockSubscribe: Subscription;
|
private onUnBlockSubscribe: Subscription;
|
||||||
|
public readonly streamStore: Readable<MediaStream | null>;
|
||||||
|
public readonly statusStore: Readable<PeerStatus>;
|
||||||
|
public readonly constraintsStore: Readable<MediaStreamConstraints|null>;
|
||||||
|
|
||||||
constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, localStream: MediaStream | null) {
|
constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) {
|
||||||
super({
|
super({
|
||||||
initiator: initiator ? initiator : false,
|
initiator: initiator ? initiator : false,
|
||||||
//reconnectTimer: 10000,
|
//reconnectTimer: 10000,
|
||||||
@ -46,7 +52,68 @@ export class VideoPeer extends Peer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.userId = user.userId;
|
this.userId = user.userId;
|
||||||
this.userName = user.name || '';
|
this.uniqueId = 'video_'+this.userId;
|
||||||
|
|
||||||
|
this.streamStore = readable<MediaStream|null>(null, (set) => {
|
||||||
|
const onStream = (stream: MediaStream|null) => {
|
||||||
|
set(stream);
|
||||||
|
};
|
||||||
|
const onData = (chunk: Buffer) => {
|
||||||
|
this.on('data', (chunk: Buffer) => {
|
||||||
|
const message = JSON.parse(chunk.toString('utf8'));
|
||||||
|
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||||
|
if (!message.video) {
|
||||||
|
set(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on('stream', onStream);
|
||||||
|
this.on('data', onData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.off('stream', onStream);
|
||||||
|
this.off('data', onData);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.constraintsStore = readable<MediaStreamConstraints|null>(null, (set) => {
|
||||||
|
const onData = (chunk: Buffer) => {
|
||||||
|
const message = JSON.parse(chunk.toString('utf8'));
|
||||||
|
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
|
||||||
|
set(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on('data', onData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.off('data', onData);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.statusStore = readable<PeerStatus>("connecting", (set) => {
|
||||||
|
const onConnect = () => {
|
||||||
|
set('connected');
|
||||||
|
};
|
||||||
|
const onError = () => {
|
||||||
|
set('error');
|
||||||
|
};
|
||||||
|
const onClose = () => {
|
||||||
|
set('closed');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.on('connect', onConnect);
|
||||||
|
this.on('error', onError);
|
||||||
|
this.on('close', onClose);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.off('connect', onConnect);
|
||||||
|
this.off('error', onError);
|
||||||
|
this.off('close', onClose);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
//start listen signal for the peer connection
|
//start listen signal for the peer connection
|
||||||
this.on('signal', (data: unknown) => {
|
this.on('signal', (data: unknown) => {
|
||||||
@ -69,8 +136,6 @@ export class VideoPeer extends Peer {
|
|||||||
|
|
||||||
this.on('connect', () => {
|
this.on('connect', () => {
|
||||||
this._connected = true;
|
this._connected = true;
|
||||||
mediaManager.isConnected("" + this.userId);
|
|
||||||
console.info(`connect => ${this.userId}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('data', (chunk: Buffer) => {
|
this.on('data', (chunk: Buffer) => {
|
||||||
@ -152,7 +217,6 @@ export class VideoPeer extends Peer {
|
|||||||
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
|
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
|
||||||
this.toggleRemoteStream(false);
|
this.toggleRemoteStream(false);
|
||||||
}
|
}
|
||||||
mediaManager.addStreamRemoteVideo("" + this.userId, stream);
|
|
||||||
}catch (err){
|
}catch (err){
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
@ -169,7 +233,7 @@ export class VideoPeer extends Peer {
|
|||||||
}
|
}
|
||||||
this.onBlockSubscribe.unsubscribe();
|
this.onBlockSubscribe.unsubscribe();
|
||||||
this.onUnBlockSubscribe.unsubscribe();
|
this.onUnBlockSubscribe.unsubscribe();
|
||||||
mediaManager.removeActiveVideo("" + this.userId);
|
discussionManager.removeParticipant(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.
|
||||||
super.destroy(error);
|
super.destroy(error);
|
||||||
|
@ -35,102 +35,109 @@ body .message-info.info{
|
|||||||
body .message-info.warning{
|
body .message-info.warning{
|
||||||
background: #ffa500d6;
|
background: #ffa500d6;
|
||||||
}
|
}
|
||||||
.video-container{
|
|
||||||
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
background-color: #00000099;
|
background-color: #00000099;
|
||||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||||
}
|
|
||||||
.video-container i{
|
|
||||||
position: absolute;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
left: calc(50% - 50px);
|
|
||||||
top: calc(50% - 50px);
|
|
||||||
background-color: black;
|
|
||||||
border-radius: 50%;
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 32px;
|
|
||||||
font-size: 28px;
|
|
||||||
color: white;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container img{
|
video {
|
||||||
position: absolute;
|
width: 100%;
|
||||||
display: none;
|
height: 100%;
|
||||||
width: 40px;
|
max-height: 90vh;
|
||||||
height: 40px;
|
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||||
left: 5px;
|
}
|
||||||
bottom: 5px;
|
|
||||||
padding: 10px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.video-container img.block-logo {
|
|
||||||
left: 30%;
|
|
||||||
bottom: 15%;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container button.report{
|
i {
|
||||||
display: block;
|
position: absolute;
|
||||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
width: 100px;
|
||||||
background: none;
|
height: 100px;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
left: calc(50% - 50px);
|
||||||
border: none;
|
top: calc(50% - 50px);
|
||||||
background-color: black;
|
background-color: black;
|
||||||
border-radius: 15px;
|
border-radius: 50%;
|
||||||
position: absolute;
|
text-align: center;
|
||||||
width: 0px;
|
padding-top: 32px;
|
||||||
height: 35px;
|
font-size: 28px;
|
||||||
right: 5px;
|
color: white;
|
||||||
bottom: 5px;
|
overflow: hidden;
|
||||||
padding: 0px;
|
}
|
||||||
overflow: hidden;
|
|
||||||
z-index: 2;
|
|
||||||
transition: all .5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container:hover button.report{
|
img {
|
||||||
width: 35px;
|
position: absolute;
|
||||||
padding: 10px;
|
display: none;
|
||||||
}
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
left: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.video-container button.report:hover {
|
img.block-logo {
|
||||||
width: 160px;
|
left: 30%;
|
||||||
}
|
bottom: 15%;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
.video-container button.report img{
|
button.report{
|
||||||
position: absolute;
|
display: block;
|
||||||
display: block;
|
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||||
bottom: 5px;
|
background: none;
|
||||||
left: 5px;
|
background-color: rgba(0, 0, 0, 0);
|
||||||
margin: 0;
|
border: none;
|
||||||
padding: 0;
|
background-color: black;
|
||||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
border-radius: 15px;
|
||||||
width: 25px;
|
position: absolute;
|
||||||
height: 25px;
|
width: 0px;
|
||||||
}
|
height: 35px;
|
||||||
.video-container button.report span{
|
right: 5px;
|
||||||
position: absolute;
|
bottom: 5px;
|
||||||
bottom: 6px;
|
padding: 0px;
|
||||||
left: 36px;
|
overflow: hidden;
|
||||||
color: white;
|
z-index: 2;
|
||||||
font-size: 16px;
|
transition: all .5s ease;
|
||||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
|
||||||
}
|
|
||||||
.video-container img.active {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container video{
|
img{
|
||||||
height: 100%;
|
position: absolute;
|
||||||
cursor: url('./images/cursor_pointer.png'), pointer;
|
display: block;
|
||||||
}
|
bottom: 5px;
|
||||||
|
left: 5px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
.video-container video:focus{
|
span {
|
||||||
outline: none;
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 36px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: url('./images/cursor_pointer.png'), pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.active {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover button.report{
|
||||||
|
width: 35px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video:focus{
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container.div-myCamVideo{
|
.video-container.div-myCamVideo{
|
||||||
@ -204,7 +211,7 @@ video.myCamVideo{
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
width: 180px;
|
width: 240px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
@ -224,7 +231,7 @@ video.myCamVideo{
|
|||||||
background: #666;
|
background: #666;
|
||||||
box-shadow: 2px 2px 24px #444;
|
box-shadow: 2px 2px 24px #444;
|
||||||
border-radius: 48px;
|
border-radius: 48px;
|
||||||
transform: translateY(20px);
|
transform: translateY(15px);
|
||||||
transition-timing-function: ease-in-out;
|
transition-timing-function: ease-in-out;
|
||||||
margin: 0 4%;
|
margin: 0 4%;
|
||||||
}
|
}
|
||||||
@ -263,6 +270,16 @@ video.myCamVideo{
|
|||||||
.btn-cam-action:hover .btn-monitor.hide{
|
.btn-cam-action:hover .btn-monitor.hide{
|
||||||
transform: translateY(60px);
|
transform: translateY(60px);
|
||||||
}
|
}
|
||||||
|
.btn-layout{
|
||||||
|
pointer-events: auto;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.btn-layout.hide {
|
||||||
|
transform: translateY(60px);
|
||||||
|
}
|
||||||
|
.btn-cam-action:hover .btn-layout.hide{
|
||||||
|
transform: translateY(60px);
|
||||||
|
}
|
||||||
.btn-copy{
|
.btn-copy{
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transition: all .3s;
|
transition: all .3s;
|
||||||
|
@ -95,6 +95,7 @@ module.exports = {
|
|||||||
|
|
||||||
if (warning.code === 'a11y-no-onchange') { return }
|
if (warning.code === 'a11y-no-onchange') { return }
|
||||||
if (warning.code === 'a11y-autofocus') { return }
|
if (warning.code === 'a11y-autofocus') { return }
|
||||||
|
if (warning.code === 'a11y-media-has-caption') { return }
|
||||||
|
|
||||||
// process as usual
|
// process as usual
|
||||||
handleWarning(warning);
|
handleWarning(warning);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// lib/app.ts
|
// lib/app.ts
|
||||||
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
|
import { IoSocketController } from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
|
||||||
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
|
import { AuthenticateController } from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
|
||||||
import {MapController} from "./Controller/MapController";
|
import { MapController } from "./Controller/MapController";
|
||||||
import {PrometheusController} from "./Controller/PrometheusController";
|
import { PrometheusController } from "./Controller/PrometheusController";
|
||||||
import {DebugController} from "./Controller/DebugController";
|
import { DebugController } from "./Controller/DebugController";
|
||||||
import {App as uwsApp} from "./Server/sifrr.server";
|
import { App as uwsApp } from "./Server/sifrr.server";
|
||||||
import {AdminController} from "./Controller/AdminController";
|
import { AdminController } from "./Controller/AdminController";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public app: uwsApp;
|
public app: uwsApp;
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import {BaseController} from "./BaseController";
|
import { BaseController } from "./BaseController";
|
||||||
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
||||||
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||||
import {apiClientRepository} from "../Services/ApiClientRepository";
|
import { apiClientRepository } from "../Services/ApiClientRepository";
|
||||||
import {AdminRoomMessage, WorldFullWarningToRoomMessage, RefreshRoomPromptMessage} from "../Messages/generated/messages_pb";
|
import {
|
||||||
|
AdminRoomMessage,
|
||||||
|
WorldFullWarningToRoomMessage,
|
||||||
|
RefreshRoomPromptMessage,
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
|
export class AdminController extends BaseController {
|
||||||
export class AdminController extends BaseController{
|
constructor(private App: TemplatedApp) {
|
||||||
|
|
||||||
constructor(private App : TemplatedApp) {
|
|
||||||
super();
|
super();
|
||||||
this.App = App;
|
this.App = App;
|
||||||
this.receiveGlobalMessagePrompt();
|
this.receiveGlobalMessagePrompt();
|
||||||
this.receiveRoomEditionPrompt();
|
this.receiveRoomEditionPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveRoomEditionPrompt() {
|
receiveRoomEditionPrompt() {
|
||||||
this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => {
|
this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => {
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
@ -23,25 +25,25 @@ export class AdminController extends BaseController{
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => {
|
this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => {
|
||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
console.warn('/message request was aborted');
|
console.warn("/message request was aborted");
|
||||||
})
|
});
|
||||||
|
|
||||||
const token = req.getHeader('admin-token');
|
const token = req.getHeader("admin-token");
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
if (token !== ADMIN_API_TOKEN) {
|
if (token !== ADMIN_API_TOKEN) {
|
||||||
console.error('Admin access refused for token: '+token)
|
console.error("Admin access refused for token: " + token);
|
||||||
res.writeStatus("401 Unauthorized").end('Incorrect token');
|
res.writeStatus("401 Unauthorized").end("Incorrect token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof body.roomId !== 'string') {
|
if (typeof body.roomId !== "string") {
|
||||||
throw 'Incorrect roomId parameter'
|
throw "Incorrect roomId parameter";
|
||||||
}
|
}
|
||||||
const roomId: string = body.roomId;
|
const roomId: string = body.roomId;
|
||||||
|
|
||||||
await apiClientRepository.getClient(roomId).then((roomClient) =>{
|
await apiClientRepository.getClient(roomId).then((roomClient) => {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
const roomMessage = new RefreshRoomPromptMessage();
|
const roomMessage = new RefreshRoomPromptMessage();
|
||||||
roomMessage.setRoomid(roomId);
|
roomMessage.setRoomid(roomId);
|
||||||
@ -57,12 +59,10 @@ export class AdminController extends BaseController{
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.writeStatus("200");
|
res.writeStatus("200");
|
||||||
res.end('ok');
|
res.end("ok");
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveGlobalMessagePrompt() {
|
receiveGlobalMessagePrompt() {
|
||||||
this.App.options("/message", (res: HttpResponse, req: HttpRequest) => {
|
this.App.options("/message", (res: HttpResponse, req: HttpRequest) => {
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
@ -71,59 +71,57 @@ export class AdminController extends BaseController{
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => {
|
this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
|
||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
console.warn('/message request was aborted');
|
console.warn("/message request was aborted");
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const token = req.getHeader("admin-token");
|
||||||
const token = req.getHeader('admin-token');
|
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
if (token !== ADMIN_API_TOKEN) {
|
if (token !== ADMIN_API_TOKEN) {
|
||||||
console.error('Admin access refused for token: '+token)
|
console.error("Admin access refused for token: " + token);
|
||||||
res.writeStatus("401 Unauthorized").end('Incorrect token');
|
res.writeStatus("401 Unauthorized").end("Incorrect token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof body.text !== 'string') {
|
if (typeof body.text !== "string") {
|
||||||
throw 'Incorrect text parameter'
|
throw "Incorrect text parameter";
|
||||||
}
|
}
|
||||||
if (body.type !== 'capacity' && body.type !== 'message') {
|
if (body.type !== "capacity" && body.type !== "message") {
|
||||||
throw 'Incorrect type parameter'
|
throw "Incorrect type parameter";
|
||||||
}
|
}
|
||||||
if (!body.targets || typeof body.targets !== 'object') {
|
if (!body.targets || typeof body.targets !== "object") {
|
||||||
throw 'Incorrect targets parameter'
|
throw "Incorrect targets parameter";
|
||||||
}
|
}
|
||||||
const text: string = body.text;
|
const text: string = body.text;
|
||||||
const type: string = body.type;
|
const type: string = body.type;
|
||||||
const targets: string[] = body.targets;
|
const targets: string[] = body.targets;
|
||||||
|
|
||||||
await Promise.all(targets.map((roomId) => {
|
await Promise.all(
|
||||||
return apiClientRepository.getClient(roomId).then((roomClient) =>{
|
targets.map((roomId) => {
|
||||||
return new Promise((res, rej) => {
|
return apiClientRepository.getClient(roomId).then((roomClient) => {
|
||||||
if (type === 'message') {
|
return new Promise((res, rej) => {
|
||||||
const roomMessage = new AdminRoomMessage();
|
if (type === "message") {
|
||||||
roomMessage.setMessage(text);
|
const roomMessage = new AdminRoomMessage();
|
||||||
roomMessage.setRoomid(roomId);
|
roomMessage.setMessage(text);
|
||||||
|
roomMessage.setRoomid(roomId);
|
||||||
|
|
||||||
roomClient.sendAdminMessageToRoom(roomMessage, (err) => {
|
roomClient.sendAdminMessageToRoom(roomMessage, (err) => {
|
||||||
err ? rej(err) : res();
|
err ? rej(err) : res();
|
||||||
});
|
});
|
||||||
} else if (type === 'capacity') {
|
} else if (type === "capacity") {
|
||||||
const roomMessage = new WorldFullWarningToRoomMessage();
|
const roomMessage = new WorldFullWarningToRoomMessage();
|
||||||
roomMessage.setRoomid(roomId);
|
roomMessage.setRoomid(roomId);
|
||||||
|
|
||||||
roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => {
|
|
||||||
err ? rej(err) : res();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => {
|
||||||
|
err ? rej(err) : res();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
}));
|
);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.errorToResponse(err, res);
|
this.errorToResponse(err, res);
|
||||||
return;
|
return;
|
||||||
@ -131,7 +129,7 @@ export class AdminController extends BaseController{
|
|||||||
|
|
||||||
res.writeStatus("200");
|
res.writeStatus("200");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end('ok');
|
res.end("ok");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from "uuid";
|
||||||
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
||||||
import {BaseController} from "./BaseController";
|
import { BaseController } from "./BaseController";
|
||||||
import {adminApi} from "../Services/AdminApi";
|
import { adminApi } from "../Services/AdminApi";
|
||||||
import {jwtTokenManager} from "../Services/JWTTokenManager";
|
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
||||||
import {parse} from "query-string";
|
import { parse } from "query-string";
|
||||||
|
|
||||||
export interface TokenInterface {
|
export interface TokenInterface {
|
||||||
userUuid: string
|
userUuid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthenticateController extends BaseController {
|
export class AuthenticateController extends BaseController {
|
||||||
|
constructor(private App: TemplatedApp) {
|
||||||
constructor(private App : TemplatedApp) {
|
|
||||||
super();
|
super();
|
||||||
this.register();
|
this.register();
|
||||||
this.verify();
|
this.verify();
|
||||||
@ -19,7 +18,7 @@ export class AuthenticateController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Try to login with an admin token
|
//Try to login with an admin token
|
||||||
private register(){
|
private register() {
|
||||||
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
|
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
|
|
||||||
@ -29,15 +28,15 @@ export class AuthenticateController extends BaseController {
|
|||||||
this.App.post("/register", (res: HttpResponse, req: HttpRequest) => {
|
this.App.post("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
console.warn('Login request was aborted');
|
console.warn("Login request was aborted");
|
||||||
})
|
});
|
||||||
const param = await res.json();
|
const param = await res.json();
|
||||||
|
|
||||||
//todo: what to do if the organizationMemberToken is already used?
|
//todo: what to do if the organizationMemberToken is already used?
|
||||||
const organizationMemberToken:string|null = param.organizationMemberToken;
|
const organizationMemberToken: string | null = param.organizationMemberToken;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof organizationMemberToken != 'string') throw new Error('No organization token');
|
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
|
||||||
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
||||||
const userUuid = data.userUuid;
|
const userUuid = data.userUuid;
|
||||||
const organizationSlug = data.organizationSlug;
|
const organizationSlug = data.organizationSlug;
|
||||||
@ -49,28 +48,26 @@ export class AuthenticateController extends BaseController {
|
|||||||
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
||||||
res.writeStatus("200 OK");
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end(JSON.stringify({
|
res.end(
|
||||||
authToken,
|
JSON.stringify({
|
||||||
userUuid,
|
authToken,
|
||||||
organizationSlug,
|
userUuid,
|
||||||
worldSlug,
|
organizationSlug,
|
||||||
roomSlug,
|
worldSlug,
|
||||||
mapUrlStart,
|
roomSlug,
|
||||||
organizationMemberToken,
|
mapUrlStart,
|
||||||
textures
|
organizationMemberToken,
|
||||||
}));
|
textures,
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.errorToResponse(e, res);
|
this.errorToResponse(e, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private verify(){
|
private verify() {
|
||||||
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
|
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
|
|
||||||
@ -82,50 +79,55 @@ export class AuthenticateController extends BaseController {
|
|||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
console.warn('verify request was aborted');
|
console.warn("verify request was aborted");
|
||||||
})
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await jwtTokenManager.getUserUuidFromToken(query.token as string);
|
await jwtTokenManager.getUserUuidFromToken(query.token as string);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.writeStatus("400 Bad Request");
|
res.writeStatus("400 Bad Request");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end(JSON.stringify({
|
res.end(
|
||||||
"success": false,
|
JSON.stringify({
|
||||||
"message": "Invalid JWT token"
|
success: false,
|
||||||
}));
|
message: "Invalid JWT token",
|
||||||
|
})
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.writeStatus("200 OK");
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end(JSON.stringify({
|
res.end(
|
||||||
"success": true
|
JSON.stringify({
|
||||||
}));
|
success: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//permit to login on application. Return token to connect on Websocket IO.
|
//permit to login on application. Return token to connect on Websocket IO.
|
||||||
private anonymLogin(){
|
private anonymLogin() {
|
||||||
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
console.warn('Login request was aborted');
|
console.warn("Login request was aborted");
|
||||||
})
|
});
|
||||||
|
|
||||||
const userUuid = v4();
|
const userUuid = v4();
|
||||||
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
||||||
res.writeStatus("200 OK");
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end(JSON.stringify({
|
res.end(
|
||||||
authToken,
|
JSON.stringify({
|
||||||
userUuid,
|
authToken,
|
||||||
}));
|
userUuid,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import {HttpResponse} from "uWebSockets.js";
|
import { HttpResponse } from "uWebSockets.js";
|
||||||
|
|
||||||
|
|
||||||
export class BaseController {
|
export class BaseController {
|
||||||
protected addCorsHeaders(res: HttpResponse): void {
|
protected addCorsHeaders(res: HttpResponse): void {
|
||||||
res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
|
||||||
res.writeHeader('access-control-allow-origin', '*');
|
res.writeHeader("access-control-allow-origin", "*");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,23 +15,23 @@ export class BaseController {
|
|||||||
if (e && e.message) {
|
if (e && e.message) {
|
||||||
let url = e?.config?.url;
|
let url = e?.config?.url;
|
||||||
if (url !== undefined) {
|
if (url !== undefined) {
|
||||||
url = ' for URL: '+url;
|
url = " for URL: " + url;
|
||||||
} else {
|
} else {
|
||||||
url = '';
|
url = "";
|
||||||
}
|
}
|
||||||
console.error('ERROR: '+e.message+url);
|
console.error("ERROR: " + e.message + url);
|
||||||
} else if (typeof(e) === 'string') {
|
} else if (typeof e === "string") {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
if (e.stack) {
|
if (e.stack) {
|
||||||
console.error(e.stack);
|
console.error(e.stack);
|
||||||
}
|
}
|
||||||
if (e.response) {
|
if (e.response) {
|
||||||
res.writeStatus(e.response.status+" "+e.response.statusText);
|
res.writeStatus(e.response.status + " " + e.response.statusText);
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("An error occurred: "+e.response.status+" "+e.response.statusText);
|
res.end("An error occurred: " + e.response.status + " " + e.response.statusText);
|
||||||
} else {
|
} else {
|
||||||
res.writeStatus("500 Internal Server Error")
|
res.writeStatus("500 Internal Server Error");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("An error occurred");
|
res.end("An error occurred");
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,46 @@
|
|||||||
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||||
import {IoSocketController} from "_Controller/IoSocketController";
|
import { IoSocketController } from "_Controller/IoSocketController";
|
||||||
import {stringify} from "circular-json";
|
import { stringify } from "circular-json";
|
||||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||||
import { parse } from 'query-string';
|
import { parse } from "query-string";
|
||||||
import {App} from "../Server/sifrr.server";
|
import { App } from "../Server/sifrr.server";
|
||||||
import {socketManager} from "../Services/SocketManager";
|
import { socketManager } from "../Services/SocketManager";
|
||||||
|
|
||||||
export class DebugController {
|
export class DebugController {
|
||||||
constructor(private App : App) {
|
constructor(private App: App) {
|
||||||
this.getDump();
|
this.getDump();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDump() {
|
||||||
getDump(){
|
|
||||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
if (query.token !== ADMIN_API_TOKEN) {
|
if (query.token !== ADMIN_API_TOKEN) {
|
||||||
return res.status(401).send('Invalid token sent!');
|
return res.status(401).send("Invalid token sent!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify(
|
return res
|
||||||
socketManager.getWorlds(),
|
.writeStatus("200 OK")
|
||||||
(key: unknown, value: unknown) => {
|
.writeHeader("Content-Type", "application/json")
|
||||||
if(value instanceof Map) {
|
.end(
|
||||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
|
||||||
for (const [mapKey, mapValue] of value.entries()) {
|
if (value instanceof Map) {
|
||||||
obj[mapKey] = mapValue;
|
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
}
|
for (const [mapKey, mapValue] of value.entries()) {
|
||||||
return obj;
|
obj[mapKey] = mapValue;
|
||||||
} else if(value instanceof Set) {
|
}
|
||||||
|
return obj;
|
||||||
|
} else if (value instanceof Set) {
|
||||||
const obj: Array<unknown> = [];
|
const obj: Array<unknown> = [];
|
||||||
for (const [setKey, setValue] of value.entries()) {
|
for (const [setKey, setValue] of value.entries()) {
|
||||||
obj.push(setValue);
|
obj.push(setValue);
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
));
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
|
||||||
import {GameRoomPolicyTypes} from "../Model/PusherRoom";
|
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
|
||||||
import {PointInterface} from "../Model/Websocket/PointInterface";
|
import { PointInterface } from "../Model/Websocket/PointInterface";
|
||||||
import {
|
import {
|
||||||
SetPlayerDetailsMessage,
|
SetPlayerDetailsMessage,
|
||||||
SubMessage,
|
SubMessage,
|
||||||
@ -18,17 +18,17 @@ import {
|
|||||||
CompanionMessage,
|
CompanionMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import {UserMovesMessage} from "../Messages/generated/messages_pb";
|
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
||||||
import {TemplatedApp} from "uWebSockets.js"
|
import { TemplatedApp } from "uWebSockets.js";
|
||||||
import {parse} from "query-string";
|
import { parse } from "query-string";
|
||||||
import {jwtTokenManager} from "../Services/JWTTokenManager";
|
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
||||||
import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi";
|
import { adminApi, CharacterTexture, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
|
||||||
import {SocketManager, socketManager} from "../Services/SocketManager";
|
import { SocketManager, socketManager } from "../Services/SocketManager";
|
||||||
import {emitInBatch} from "../Services/IoSocketHelpers";
|
import { emitInBatch } from "../Services/IoSocketHelpers";
|
||||||
import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
|
||||||
import {Zone} from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface";
|
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
||||||
import {v4} from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export class IoSocketController {
|
export class IoSocketController {
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
@ -39,32 +39,29 @@ export class IoSocketController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
adminRoomSocket() {
|
adminRoomSocket() {
|
||||||
this.app.ws('/admin/rooms', {
|
this.app.ws("/admin/rooms", {
|
||||||
upgrade: (res, req, context) => {
|
upgrade: (res, req, context) => {
|
||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
const websocketKey = req.getHeader('sec-websocket-key');
|
const websocketKey = req.getHeader("sec-websocket-key");
|
||||||
const websocketProtocol = req.getHeader('sec-websocket-protocol');
|
const websocketProtocol = req.getHeader("sec-websocket-protocol");
|
||||||
const websocketExtensions = req.getHeader('sec-websocket-extensions');
|
const websocketExtensions = req.getHeader("sec-websocket-extensions");
|
||||||
const token = query.token;
|
const token = query.token;
|
||||||
if (token !== ADMIN_API_TOKEN) {
|
if (token !== ADMIN_API_TOKEN) {
|
||||||
console.log('Admin access refused for token: '+token)
|
console.log("Admin access refused for token: " + token);
|
||||||
res.writeStatus("401 Unauthorized").end('Incorrect token');
|
res.writeStatus("401 Unauthorized").end("Incorrect token");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const roomId = query.roomId;
|
const roomId = query.roomId;
|
||||||
if (typeof roomId !== 'string') {
|
if (typeof roomId !== "string") {
|
||||||
console.error('Received')
|
console.error("Received");
|
||||||
res.writeStatus("400 Bad Request").end('Missing room id');
|
res.writeStatus("400 Bad Request").end("Missing room id");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.upgrade(
|
res.upgrade({ roomId }, websocketKey, websocketProtocol, websocketExtensions, context);
|
||||||
{roomId},
|
|
||||||
websocketKey, websocketProtocol, websocketExtensions, context,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
open: (ws) => {
|
open: (ws) => {
|
||||||
console.log('Admin socket connect for room: '+ws.roomId);
|
console.log("Admin socket connect for room: " + ws.roomId);
|
||||||
ws.disconnecting = false;
|
ws.disconnecting = false;
|
||||||
|
|
||||||
socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string);
|
socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string);
|
||||||
@ -74,24 +71,34 @@ export class IoSocketController {
|
|||||||
const roomId = ws.roomId as string;
|
const roomId = ws.roomId as string;
|
||||||
|
|
||||||
//TODO refactor message type and data
|
//TODO refactor message type and data
|
||||||
const message: {event: string, message: {type: string, message: unknown, userUuid: string}} =
|
const message: { event: string; message: { type: string; message: unknown; userUuid: string } } =
|
||||||
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
|
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
|
||||||
|
|
||||||
if(message.event === 'user-message') {
|
if (message.event === "user-message") {
|
||||||
const messageToEmit = (message.message as { message: string, type: string, userUuid: string });
|
const messageToEmit = message.message as { message: string; type: string; userUuid: string };
|
||||||
if(messageToEmit.type === 'banned'){
|
if (messageToEmit.type === "banned") {
|
||||||
socketManager.emitBan(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string);
|
socketManager.emitBan(
|
||||||
|
messageToEmit.userUuid,
|
||||||
|
messageToEmit.message,
|
||||||
|
messageToEmit.type,
|
||||||
|
ws.roomId as string
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if(messageToEmit.type === 'ban') {
|
if (messageToEmit.type === "ban") {
|
||||||
socketManager.emitSendUserMessage(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string);
|
socketManager.emitSendUserMessage(
|
||||||
|
messageToEmit.userUuid,
|
||||||
|
messageToEmit.message,
|
||||||
|
messageToEmit.type,
|
||||||
|
ws.roomId as string
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close: (ws, code, message) => {
|
close: (ws, code, message) => {
|
||||||
const Client = (ws as ExAdminSocketInterface);
|
const Client = ws as ExAdminSocketInterface;
|
||||||
try {
|
try {
|
||||||
Client.disconnecting = true;
|
Client.disconnecting = true;
|
||||||
socketManager.leaveAdminRoom(Client);
|
socketManager.leaveAdminRoom(Client);
|
||||||
@ -99,12 +106,12 @@ export class IoSocketController {
|
|||||||
console.error('An error occurred on admin "disconnect"');
|
console.error('An error occurred on admin "disconnect"');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ioConnection() {
|
ioConnection() {
|
||||||
this.app.ws('/room', {
|
this.app.ws("/room", {
|
||||||
/* Options */
|
/* Options */
|
||||||
//compression: uWS.SHARED_COMPRESSOR,
|
//compression: uWS.SHARED_COMPRESSOR,
|
||||||
idleTimeout: SOCKET_IDLE_TIMER,
|
idleTimeout: SOCKET_IDLE_TIMER,
|
||||||
@ -114,7 +121,7 @@ export class IoSocketController {
|
|||||||
upgrade: (res, req, context) => {
|
upgrade: (res, req, context) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
/* Keep track of abortions */
|
/* Keep track of abortions */
|
||||||
const upgradeAborted = {aborted: false};
|
const upgradeAborted = { aborted: false };
|
||||||
|
|
||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
/* We can simply signal that we were aborted */
|
/* We can simply signal that we were aborted */
|
||||||
@ -123,15 +130,15 @@ export class IoSocketController {
|
|||||||
|
|
||||||
const url = req.getUrl();
|
const url = req.getUrl();
|
||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
const websocketKey = req.getHeader('sec-websocket-key');
|
const websocketKey = req.getHeader("sec-websocket-key");
|
||||||
const websocketProtocol = req.getHeader('sec-websocket-protocol');
|
const websocketProtocol = req.getHeader("sec-websocket-protocol");
|
||||||
const websocketExtensions = req.getHeader('sec-websocket-extensions');
|
const websocketExtensions = req.getHeader("sec-websocket-extensions");
|
||||||
const IPAddress = req.getHeader('x-forwarded-for');
|
const IPAddress = req.getHeader("x-forwarded-for");
|
||||||
|
|
||||||
const roomId = query.roomId;
|
const roomId = query.roomId;
|
||||||
try {
|
try {
|
||||||
if (typeof roomId !== 'string') {
|
if (typeof roomId !== "string") {
|
||||||
throw new Error('Undefined room ID: ');
|
throw new Error("Undefined room ID: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = query.token;
|
const token = query.token;
|
||||||
@ -143,62 +150,69 @@ export class IoSocketController {
|
|||||||
const right = Number(query.right);
|
const right = Number(query.right);
|
||||||
const name = query.name;
|
const name = query.name;
|
||||||
|
|
||||||
let companion: CompanionMessage|undefined = undefined;
|
let companion: CompanionMessage | undefined = undefined;
|
||||||
|
|
||||||
if (typeof query.companion === 'string') {
|
if (typeof query.companion === "string") {
|
||||||
companion = new CompanionMessage();
|
companion = new CompanionMessage();
|
||||||
companion.setName(query.companion);
|
companion.setName(query.companion);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof name !== 'string') {
|
if (typeof name !== "string") {
|
||||||
throw new Error('Expecting name');
|
throw new Error("Expecting name");
|
||||||
}
|
}
|
||||||
if (name === '') {
|
if (name === "") {
|
||||||
throw new Error('No empty name');
|
throw new Error("No empty name");
|
||||||
}
|
}
|
||||||
let characterLayers = query.characterLayers;
|
let characterLayers = query.characterLayers;
|
||||||
if (characterLayers === null) {
|
if (characterLayers === null) {
|
||||||
throw new Error('Expecting skin');
|
throw new Error("Expecting skin");
|
||||||
}
|
}
|
||||||
if (typeof characterLayers === 'string') {
|
if (typeof characterLayers === "string") {
|
||||||
characterLayers = [ characterLayers ];
|
characterLayers = [characterLayers];
|
||||||
}
|
}
|
||||||
|
|
||||||
const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId);
|
const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId);
|
||||||
|
|
||||||
let memberTags: string[] = [];
|
let memberTags: string[] = [];
|
||||||
let memberVisitCardUrl: string|null = null;
|
let memberVisitCardUrl: string | null = null;
|
||||||
let memberMessages: unknown;
|
let memberMessages: unknown;
|
||||||
let memberTextures: CharacterTexture[] = [];
|
let memberTextures: CharacterTexture[] = [];
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
const room = await socketManager.getOrCreateRoom(roomId);
|
||||||
if (ADMIN_API_URL) {
|
if (ADMIN_API_URL) {
|
||||||
try {
|
try {
|
||||||
let userData : FetchMemberDataByUuidResponse = {
|
let userData: FetchMemberDataByUuidResponse = {
|
||||||
uuid: v4(),
|
uuid: v4(),
|
||||||
tags: [],
|
tags: [],
|
||||||
visitCardUrl: null,
|
visitCardUrl: null,
|
||||||
textures: [],
|
textures: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
anonymous: true
|
anonymous: true,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId);
|
userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId);
|
||||||
}catch (err){
|
} catch (err) {
|
||||||
if (err?.response?.status == 404) {
|
if (err?.response?.status == 404) {
|
||||||
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
|
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
|
||||||
console.warn('Cannot find user with uuid "'+userUuid+'". Performing an anonymous login instead.');
|
console.warn(
|
||||||
} else if(err?.response?.status == 403) {
|
'Cannot find user with uuid "' +
|
||||||
|
userUuid +
|
||||||
|
'". Performing an anonymous login instead.'
|
||||||
|
);
|
||||||
|
} else if (err?.response?.status == 403) {
|
||||||
// If we get an HTTP 403, the world is full. We need to broadcast a special error to the client.
|
// If we get an HTTP 403, the world is full. We need to broadcast a special error to the client.
|
||||||
// we finish immediately the upgrade then we will close the socket as soon as it starts opening.
|
// we finish immediately the upgrade then we will close the socket as soon as it starts opening.
|
||||||
return res.upgrade({
|
return res.upgrade(
|
||||||
rejected: true,
|
{
|
||||||
message: err?.response?.data.message,
|
rejected: true,
|
||||||
status: err?.response?.status
|
message: err?.response?.data.message,
|
||||||
}, websocketKey,
|
status: err?.response?.status,
|
||||||
websocketProtocol,
|
},
|
||||||
websocketExtensions,
|
websocketKey,
|
||||||
context);
|
websocketProtocol,
|
||||||
}else{
|
websocketExtensions,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,21 +220,30 @@ export class IoSocketController {
|
|||||||
memberTags = userData.tags;
|
memberTags = userData.tags;
|
||||||
memberVisitCardUrl = userData.visitCardUrl;
|
memberVisitCardUrl = userData.visitCardUrl;
|
||||||
memberTextures = userData.textures;
|
memberTextures = userData.textures;
|
||||||
if (!room.public && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags))) {
|
if (
|
||||||
throw new Error('Insufficient privileges to access this room')
|
!room.public &&
|
||||||
|
room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY &&
|
||||||
|
(userData.anonymous === true || !room.canAccess(memberTags))
|
||||||
|
) {
|
||||||
|
throw new Error("Insufficient privileges to access this room");
|
||||||
}
|
}
|
||||||
if (!room.public && room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && userData.anonymous === true) {
|
if (
|
||||||
throw new Error('Use the login URL to connect')
|
!room.public &&
|
||||||
|
room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY &&
|
||||||
|
userData.anonymous === true
|
||||||
|
) {
|
||||||
|
throw new Error("Use the login URL to connect");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('access not granted for user '+userUuid+' and room '+roomId);
|
console.log("access not granted for user " + userUuid + " and room " + roomId);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new Error('User cannot access this world')
|
throw new Error("User cannot access this world");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate characterLayers objects from characterLayers string[]
|
// Generate characterLayers objects from characterLayers string[]
|
||||||
const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
|
const characterLayerObjs: CharacterLayer[] =
|
||||||
|
SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
|
||||||
|
|
||||||
if (upgradeAborted.aborted) {
|
if (upgradeAborted.aborted) {
|
||||||
console.log("Ouch! Client disconnected before we could upgrade it!");
|
console.log("Ouch! Client disconnected before we could upgrade it!");
|
||||||
@ -229,7 +252,8 @@ export class IoSocketController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* This immediately calls open handler, you must not use res after this call */
|
/* This immediately calls open handler, you must not use res after this call */
|
||||||
res.upgrade({
|
res.upgrade(
|
||||||
|
{
|
||||||
// Data passed here is accessible on the "websocket" socket object.
|
// Data passed here is accessible on the "websocket" socket object.
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
@ -246,22 +270,22 @@ export class IoSocketController {
|
|||||||
position: {
|
position: {
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
direction: 'down',
|
direction: "down",
|
||||||
moving: false
|
moving: false,
|
||||||
} as PointInterface,
|
} as PointInterface,
|
||||||
viewport: {
|
viewport: {
|
||||||
top,
|
top,
|
||||||
right,
|
right,
|
||||||
bottom,
|
bottom,
|
||||||
left
|
left,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
/* Spell these correctly */
|
/* Spell these correctly */
|
||||||
websocketKey,
|
websocketKey,
|
||||||
websocketProtocol,
|
websocketProtocol,
|
||||||
websocketExtensions,
|
websocketExtensions,
|
||||||
context);
|
context
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/*if (e instanceof Error) {
|
/*if (e instanceof Error) {
|
||||||
console.log(e.message);
|
console.log(e.message);
|
||||||
@ -269,23 +293,26 @@ export class IoSocketController {
|
|||||||
} else {
|
} else {
|
||||||
res.writeStatus("500 Internal Server Error").end('An error occurred');
|
res.writeStatus("500 Internal Server Error").end('An error occurred');
|
||||||
}*/
|
}*/
|
||||||
return res.upgrade({
|
return res.upgrade(
|
||||||
rejected: true,
|
{
|
||||||
message: e.message ? e.message : '500 Internal Server Error'
|
rejected: true,
|
||||||
}, websocketKey,
|
message: e.message ? e.message : "500 Internal Server Error",
|
||||||
websocketProtocol,
|
},
|
||||||
websocketExtensions,
|
websocketKey,
|
||||||
context);
|
websocketProtocol,
|
||||||
|
websocketExtensions,
|
||||||
|
context
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
/* Handlers */
|
/* Handlers */
|
||||||
open: (ws) => {
|
open: (ws) => {
|
||||||
if(ws.rejected === true) {
|
if (ws.rejected === true) {
|
||||||
//FIX ME to use status code
|
//FIX ME to use status code
|
||||||
if(ws.message === 'World is full'){
|
if (ws.message === "World is full") {
|
||||||
socketManager.emitWorldFullMessage(ws);
|
socketManager.emitWorldFullMessage(ws);
|
||||||
}else{
|
} else {
|
||||||
socketManager.emitConnexionErrorMessage(ws, ws.message as string);
|
socketManager.emitConnexionErrorMessage(ws, ws.message as string);
|
||||||
}
|
}
|
||||||
ws.close();
|
ws.close();
|
||||||
@ -299,7 +326,7 @@ export class IoSocketController {
|
|||||||
//get data information and show messages
|
//get data information and show messages
|
||||||
if (client.messages && Array.isArray(client.messages)) {
|
if (client.messages && Array.isArray(client.messages)) {
|
||||||
client.messages.forEach((c: unknown) => {
|
client.messages.forEach((c: unknown) => {
|
||||||
const messageToSend = c as { type: string, message: string };
|
const messageToSend = c as { type: string; message: string };
|
||||||
|
|
||||||
const sendUserMessage = new SendUserMessage();
|
const sendUserMessage = new SendUserMessage();
|
||||||
sendUserMessage.setType(messageToSend.type);
|
sendUserMessage.setType(messageToSend.type);
|
||||||
@ -323,33 +350,48 @@ export class IoSocketController {
|
|||||||
} else if (message.hasUsermovesmessage()) {
|
} else if (message.hasUsermovesmessage()) {
|
||||||
socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
|
socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
|
||||||
} else if (message.hasSetplayerdetailsmessage()) {
|
} else if (message.hasSetplayerdetailsmessage()) {
|
||||||
socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage);
|
socketManager.handleSetPlayerDetails(
|
||||||
|
client,
|
||||||
|
message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage
|
||||||
|
);
|
||||||
} else if (message.hasSilentmessage()) {
|
} else if (message.hasSilentmessage()) {
|
||||||
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
|
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
|
||||||
} else if (message.hasItemeventmessage()) {
|
} else if (message.hasItemeventmessage()) {
|
||||||
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
|
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
|
||||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||||
socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
|
socketManager.emitVideo(
|
||||||
|
client,
|
||||||
|
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||||
|
);
|
||||||
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||||
socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
|
socketManager.emitScreenSharing(
|
||||||
|
client,
|
||||||
|
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||||
|
);
|
||||||
} else if (message.hasPlayglobalmessage()) {
|
} else if (message.hasPlayglobalmessage()) {
|
||||||
socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage);
|
socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage);
|
||||||
} else if (message.hasReportplayermessage()){
|
} else if (message.hasReportplayermessage()) {
|
||||||
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
|
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
|
||||||
} else if (message.hasQueryjitsijwtmessage()){
|
} else if (message.hasQueryjitsijwtmessage()) {
|
||||||
socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
|
socketManager.handleQueryJitsiJwtMessage(
|
||||||
} else if (message.hasEmotepromptmessage()){
|
client,
|
||||||
socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage);
|
message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasEmotepromptmessage()) {
|
||||||
|
socketManager.handleEmotePromptMessage(
|
||||||
|
client,
|
||||||
|
message.getEmotepromptmessage() as EmotePromptMessage
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ok is false if backpressure was built up, wait for drain */
|
/* Ok is false if backpressure was built up, wait for drain */
|
||||||
//let ok = ws.send(message, isBinary);
|
//let ok = ws.send(message, isBinary);
|
||||||
},
|
},
|
||||||
drain: (ws) => {
|
drain: (ws) => {
|
||||||
console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
|
console.log("WebSocket backpressure: " + ws.getBufferedAmount());
|
||||||
},
|
},
|
||||||
close: (ws, code, message) => {
|
close: (ws, code, message) => {
|
||||||
const Client = (ws as ExSocketInterface);
|
const Client = ws as ExSocketInterface;
|
||||||
try {
|
try {
|
||||||
Client.disconnecting = true;
|
Client.disconnecting = true;
|
||||||
//leave room
|
//leave room
|
||||||
@ -358,13 +400,13 @@ export class IoSocketController {
|
|||||||
console.error('An error occurred on "disconnect"');
|
console.error('An error occurred on "disconnect"');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
private initClient(ws: any): ExSocketInterface {
|
private initClient(ws: any): ExSocketInterface {
|
||||||
const client : ExSocketInterface = ws;
|
const client: ExSocketInterface = ws;
|
||||||
client.userId = this.nextUserId;
|
client.userId = this.nextUserId;
|
||||||
this.nextUserId++;
|
this.nextUserId++;
|
||||||
client.userUuid = ws.userUuid;
|
client.userUuid = ws.userUuid;
|
||||||
@ -374,7 +416,7 @@ export class IoSocketController {
|
|||||||
client.batchTimeout = null;
|
client.batchTimeout = null;
|
||||||
client.emitInBatch = (payload: SubMessage): void => {
|
client.emitInBatch = (payload: SubMessage): void => {
|
||||||
emitInBatch(client, payload);
|
emitInBatch(client, payload);
|
||||||
}
|
};
|
||||||
client.disconnecting = false;
|
client.disconnecting = false;
|
||||||
|
|
||||||
client.messages = ws.messages;
|
client.messages = ws.messages;
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
||||||
import {BaseController} from "./BaseController";
|
import { BaseController } from "./BaseController";
|
||||||
import {parse} from "query-string";
|
import { parse } from "query-string";
|
||||||
import {adminApi} from "../Services/AdminApi";
|
import { adminApi } from "../Services/AdminApi";
|
||||||
|
|
||||||
|
export class MapController extends BaseController {
|
||||||
export class MapController extends BaseController{
|
constructor(private App: TemplatedApp) {
|
||||||
|
|
||||||
constructor(private App : TemplatedApp) {
|
|
||||||
super();
|
super();
|
||||||
this.App = App;
|
this.App = App;
|
||||||
this.getMapUrl();
|
this.getMapUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Returns a map mapping map name to file name of the map
|
// Returns a map mapping map name to file name of the map
|
||||||
getMapUrl() {
|
getMapUrl() {
|
||||||
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
|
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
|
||||||
@ -22,29 +19,28 @@ export class MapController extends BaseController{
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.App.get("/map", (res: HttpResponse, req: HttpRequest) => {
|
this.App.get("/map", (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
|
||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
console.warn('/map request was aborted');
|
console.warn("/map request was aborted");
|
||||||
})
|
});
|
||||||
|
|
||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
if (typeof query.organizationSlug !== 'string') {
|
if (typeof query.organizationSlug !== "string") {
|
||||||
console.error('Expected organizationSlug parameter');
|
console.error("Expected organizationSlug parameter");
|
||||||
res.writeStatus("400 Bad request");
|
res.writeStatus("400 Bad request");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("Expected organizationSlug parameter");
|
res.end("Expected organizationSlug parameter");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof query.worldSlug !== 'string') {
|
if (typeof query.worldSlug !== "string") {
|
||||||
console.error('Expected worldSlug parameter');
|
console.error("Expected worldSlug parameter");
|
||||||
res.writeStatus("400 Bad request");
|
res.writeStatus("400 Bad request");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("Expected worldSlug parameter");
|
res.end("Expected worldSlug parameter");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) {
|
if (typeof query.roomSlug !== "string" && query.roomSlug !== undefined) {
|
||||||
console.error('Expected only one roomSlug parameter');
|
console.error("Expected only one roomSlug parameter");
|
||||||
res.writeStatus("400 Bad request");
|
res.writeStatus("400 Bad request");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("Expected only one roomSlug parameter");
|
res.end("Expected only one roomSlug parameter");
|
||||||
@ -53,7 +49,11 @@ export class MapController extends BaseController{
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined);
|
const mapDetails = await adminApi.fetchMapDetails(
|
||||||
|
query.organizationSlug as string,
|
||||||
|
query.worldSlug as string,
|
||||||
|
query.roomSlug as string | undefined
|
||||||
|
);
|
||||||
|
|
||||||
res.writeStatus("200 OK");
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
@ -62,7 +62,6 @@ export class MapController extends BaseController{
|
|||||||
this.errorToResponse(e, res);
|
this.errorToResponse(e, res);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {App} from "../Server/sifrr.server";
|
import { App } from "../Server/sifrr.server";
|
||||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||||
const register = require('prom-client').register;
|
const register = require("prom-client").register;
|
||||||
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
|
const collectDefaultMetrics = require("prom-client").collectDefaultMetrics;
|
||||||
|
|
||||||
export class PrometheusController {
|
export class PrometheusController {
|
||||||
constructor(private App: App) {
|
constructor(private App: App) {
|
||||||
@ -14,7 +14,7 @@ export class PrometheusController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private metrics(res: HttpResponse, req: HttpRequest): void {
|
private metrics(res: HttpResponse, req: HttpRequest): void {
|
||||||
res.writeHeader('Content-Type', register.contentType);
|
res.writeHeader("Content-Type", register.contentType);
|
||||||
res.end(register.metrics());
|
res.end(register.metrics());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
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;
|
||||||
const API_URL = process.env.API_URL || '';
|
const API_URL = process.env.API_URL || "";
|
||||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || '';
|
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
|
||||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
|
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
|
||||||
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600;
|
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || "") || 600;
|
||||||
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
||||||
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
|
||||||
const JITSI_ISS = process.env.JITSI_ISS || '';
|
const JITSI_ISS = process.env.JITSI_ISS || "";
|
||||||
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
|
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
|
||||||
const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || '8080') || 8080
|
const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080;
|
||||||
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 {
|
export {
|
||||||
@ -26,5 +26,5 @@ export {
|
|||||||
JITSI_URL,
|
JITSI_URL,
|
||||||
JITSI_ISS,
|
JITSI_ISS,
|
||||||
SECRET_JITSI_KEY,
|
SECRET_JITSI_KEY,
|
||||||
PUSHER_HTTP_PORT
|
PUSHER_HTTP_PORT,
|
||||||
}
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A physical object that can be placed into a Zone
|
* A physical object that can be placed into a Zone
|
||||||
*/
|
*/
|
||||||
export interface Movable {
|
export interface Movable {
|
||||||
getPosition(): PositionInterface
|
getPosition(): PositionInterface;
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
||||||
* number of players around the current player.
|
* number of players around the current player.
|
||||||
*/
|
*/
|
||||||
import {Zone, ZoneEventListener} from "./Zone";
|
import { Zone, ZoneEventListener } from "./Zone";
|
||||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
||||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||||
//import Debug from "debug";
|
//import Debug from "debug";
|
||||||
|
|
||||||
//const debug = Debug('positiondispatcher');
|
//const debug = Debug('positiondispatcher');
|
||||||
@ -21,19 +21,22 @@ interface ZoneDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PositionDispatcher {
|
export class PositionDispatcher {
|
||||||
|
|
||||||
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
||||||
|
|
||||||
private zones: Zone[][] = [];
|
private zones: Zone[][] = [];
|
||||||
|
|
||||||
constructor(public readonly roomId: string, private zoneWidth: number, private zoneHeight: number, private socketListener: ZoneEventListener) {
|
constructor(
|
||||||
}
|
public readonly roomId: string,
|
||||||
|
private zoneWidth: number,
|
||||||
|
private zoneHeight: number,
|
||||||
|
private socketListener: ZoneEventListener
|
||||||
|
) {}
|
||||||
|
|
||||||
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||||
return {
|
return {
|
||||||
i: Math.floor(x / this.zoneWidth),
|
i: Math.floor(x / this.zoneWidth),
|
||||||
j: Math.floor(y / this.zoneHeight),
|
j: Math.floor(y / this.zoneHeight),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,7 +44,7 @@ export class PositionDispatcher {
|
|||||||
*/
|
*/
|
||||||
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
|
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
|
||||||
if (viewport.left > viewport.right || viewport.top > viewport.bottom) {
|
if (viewport.left > viewport.right || viewport.top > viewport.bottom) {
|
||||||
console.warn('Invalid viewport received: ', viewport);
|
console.warn("Invalid viewport received: ", viewport);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,8 +60,8 @@ export class PositionDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addedZones = [...newZones].filter(x => !oldZones.has(x));
|
const addedZones = [...newZones].filter((x) => !oldZones.has(x));
|
||||||
const removedZones = [...oldZones].filter(x => !newZones.has(x));
|
const removedZones = [...oldZones].filter((x) => !newZones.has(x));
|
||||||
|
|
||||||
for (const zone of addedZones) {
|
for (const zone of addedZones) {
|
||||||
zone.startListening(socket);
|
zone.startListening(socket);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface PositionInterface {
|
export interface PositionInterface {
|
||||||
x: number,
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||||
import {PositionDispatcher} from "./PositionDispatcher";
|
import { PositionDispatcher } from "./PositionDispatcher";
|
||||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
||||||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
|
import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
|
||||||
import {arrayIntersect} from "../Services/ArrayHelper";
|
import { arrayIntersect } from "../Services/ArrayHelper";
|
||||||
import {ZoneEventListener} from "_Model/Zone";
|
import { ZoneEventListener } from "_Model/Zone";
|
||||||
|
|
||||||
export enum GameRoomPolicyTypes {
|
export enum GameRoomPolicyTypes {
|
||||||
ANONYMUS_POLICY = 1,
|
ANONYMUS_POLICY = 1,
|
||||||
@ -17,13 +17,11 @@ export class PusherRoom {
|
|||||||
public tags: string[];
|
public tags: string[];
|
||||||
public policyType: GameRoomPolicyTypes;
|
public policyType: GameRoomPolicyTypes;
|
||||||
public readonly roomSlug: string;
|
public readonly roomSlug: string;
|
||||||
public readonly worldSlug: string = '';
|
public readonly worldSlug: string = "";
|
||||||
public readonly organizationSlug: string = '';
|
public readonly organizationSlug: string = "";
|
||||||
private versionNumber: number = 1;
|
private versionNumber: number = 1;
|
||||||
|
|
||||||
constructor(public readonly roomId: string,
|
constructor(public readonly roomId: string, private socketListener: ZoneEventListener) {
|
||||||
private socketListener: ZoneEventListener)
|
|
||||||
{
|
|
||||||
this.public = isRoomAnonymous(roomId);
|
this.public = isRoomAnonymous(roomId);
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY;
|
this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY;
|
||||||
@ -31,7 +29,7 @@ export class PusherRoom {
|
|||||||
if (this.public) {
|
if (this.public) {
|
||||||
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
||||||
} else {
|
} else {
|
||||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
|
const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
|
||||||
this.roomSlug = roomSlug;
|
this.roomSlug = roomSlug;
|
||||||
this.organizationSlug = organizationSlug;
|
this.organizationSlug = organizationSlug;
|
||||||
this.worldSlug = worldSlug;
|
this.worldSlug = worldSlug;
|
||||||
@ -41,11 +39,11 @@ export class PusherRoom {
|
|||||||
this.positionNotifier = new PositionDispatcher(this.roomId, 320, 320, this.socketListener);
|
this.positionNotifier = new PositionDispatcher(this.roomId, 320, 320, this.socketListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setViewport(socket : ExSocketInterface, viewport: ViewportInterface): void {
|
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
|
||||||
this.positionNotifier.setViewport(socket, viewport);
|
this.positionNotifier.setViewport(socket, viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
public leave(socket : ExSocketInterface){
|
public leave(socket: ExSocketInterface) {
|
||||||
this.positionNotifier.removeViewport(socket);
|
this.positionNotifier.removeViewport(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
//helper functions to parse room IDs
|
//helper functions to parse room IDs
|
||||||
|
|
||||||
export const isRoomAnonymous = (roomID: string): boolean => {
|
export const isRoomAnonymous = (roomID: string): boolean => {
|
||||||
if (roomID.startsWith('_/')) {
|
if (roomID.startsWith("_/")) {
|
||||||
return true;
|
return true;
|
||||||
} else if(roomID.startsWith('@/')) {
|
} else if (roomID.startsWith("@/")) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Incorrect room ID: '+roomID);
|
throw new Error("Incorrect room ID: " + roomID);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
||||||
const idParts = roomId.split('/');
|
const idParts = roomId.split("/");
|
||||||
if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId);
|
if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId);
|
||||||
return idParts.slice(2).join('/');
|
return idParts.slice(2).join("/");
|
||||||
}
|
};
|
||||||
export interface extractDataFromPrivateRoomIdResponse {
|
export interface extractDataFromPrivateRoomIdResponse {
|
||||||
organizationSlug: string;
|
organizationSlug: string;
|
||||||
worldSlug: string;
|
worldSlug: string;
|
||||||
roomSlug: string;
|
roomSlug: string;
|
||||||
}
|
}
|
||||||
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
||||||
const idParts = roomId.split('/');
|
const idParts = roomId.split("/");
|
||||||
if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId);
|
if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId);
|
||||||
const organizationSlug = idParts[1];
|
const organizationSlug = idParts[1];
|
||||||
const worldSlug = idParts[2];
|
const worldSlug = idParts[2];
|
||||||
const roomSlug = idParts[3];
|
const roomSlug = idParts[3];
|
||||||
return {organizationSlug, worldSlug, roomSlug}
|
return { organizationSlug, worldSlug, roomSlug };
|
||||||
}
|
};
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
import {PointInterface} from "./PointInterface";
|
import { PointInterface } from "./PointInterface";
|
||||||
import {Identificable} from "./Identificable";
|
import { Identificable } from "./Identificable";
|
||||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
||||||
import {
|
import {
|
||||||
AdminPusherToBackMessage,
|
AdminPusherToBackMessage,
|
||||||
BatchMessage,
|
BatchMessage,
|
||||||
PusherToBackMessage, ServerToAdminClientMessage,
|
PusherToBackMessage,
|
||||||
|
ServerToAdminClientMessage,
|
||||||
ServerToClientMessage,
|
ServerToClientMessage,
|
||||||
SubMessage
|
SubMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import {WebSocket} from "uWebSockets.js"
|
import { WebSocket } from "uWebSockets.js";
|
||||||
import {CharacterTexture} from "../../Services/AdminApi";
|
import { CharacterTexture } from "../../Services/AdminApi";
|
||||||
import {ClientDuplexStream} from "grpc";
|
import { ClientDuplexStream } from "grpc";
|
||||||
import {Zone} from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
|
|
||||||
export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||||
|
|
||||||
export interface ExAdminSocketInterface extends WebSocket {
|
export interface ExAdminSocketInterface extends WebSocket {
|
||||||
adminConnection: AdminConnection,
|
adminConnection: AdminConnection;
|
||||||
disconnecting: boolean,
|
disconnecting: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import {PointInterface} from "./PointInterface";
|
import { PointInterface } from "./PointInterface";
|
||||||
import {Identificable} from "./Identificable";
|
import { Identificable } from "./Identificable";
|
||||||
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
||||||
import {
|
import {
|
||||||
BatchMessage,
|
BatchMessage,
|
||||||
CompanionMessage,
|
CompanionMessage,
|
||||||
PusherToBackMessage,
|
PusherToBackMessage,
|
||||||
ServerToClientMessage,
|
ServerToClientMessage,
|
||||||
SubMessage
|
SubMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import {WebSocket} from "uWebSockets.js"
|
import { WebSocket } from "uWebSockets.js";
|
||||||
import {CharacterTexture} from "../../Services/AdminApi";
|
import { CharacterTexture } from "../../Services/AdminApi";
|
||||||
import {ClientDuplexStream} from "grpc";
|
import { ClientDuplexStream } from "grpc";
|
||||||
import {Zone} from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
|
|
||||||
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
||||||
|
|
||||||
export interface CharacterLayer {
|
export interface CharacterLayer {
|
||||||
name: string,
|
name: string;
|
||||||
url: string|undefined
|
url: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExSocketInterface extends WebSocket, Identificable {
|
export interface ExSocketInterface extends WebSocket, Identificable {
|
||||||
@ -36,12 +36,12 @@ export interface ExSocketInterface extends WebSocket, Identificable {
|
|||||||
*/
|
*/
|
||||||
emitInBatch: (payload: SubMessage) => void;
|
emitInBatch: (payload: SubMessage) => void;
|
||||||
batchedMessages: BatchMessage;
|
batchedMessages: BatchMessage;
|
||||||
batchTimeout: NodeJS.Timeout|null;
|
batchTimeout: NodeJS.Timeout | null;
|
||||||
disconnecting: boolean,
|
disconnecting: boolean;
|
||||||
messages: unknown,
|
messages: unknown;
|
||||||
tags: string[],
|
tags: string[];
|
||||||
visitCardUrl: string|null,
|
visitCardUrl: string | null;
|
||||||
textures: CharacterTexture[],
|
textures: CharacterTexture[];
|
||||||
backConnection: BackConnection,
|
backConnection: BackConnection;
|
||||||
listenedZones: Set<Zone>;
|
listenedZones: Set<Zone>;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isItemEventMessageInterface =
|
export const isItemEventMessageInterface = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
itemId: tg.isNumber,
|
itemId: tg.isNumber,
|
||||||
event: tg.isString,
|
event: tg.isString,
|
||||||
state: tg.isUnknown,
|
state: tg.isUnknown,
|
||||||
parameters: tg.isUnknown,
|
parameters: tg.isUnknown,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import {PointInterface} from "./PointInterface";
|
import { PointInterface } from "./PointInterface";
|
||||||
|
|
||||||
export class Point implements PointInterface{
|
export class Point implements PointInterface {
|
||||||
constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) {
|
constructor(
|
||||||
}
|
public x: number,
|
||||||
|
public y: number,
|
||||||
|
public direction: string = "none",
|
||||||
|
public moving: boolean = false
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,12 @@ import * as tg from "generic-type-guard";
|
|||||||
readonly moving: boolean;
|
readonly moving: boolean;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
export const isPointInterface =
|
export const isPointInterface = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
x: tg.isNumber,
|
x: tg.isNumber,
|
||||||
y: tg.isNumber,
|
y: tg.isNumber,
|
||||||
direction: tg.isString,
|
direction: tg.isString,
|
||||||
moving: tg.isBoolean
|
moving: tg.isBoolean,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
||||||
|
@ -1,34 +1,33 @@
|
|||||||
import {PointInterface} from "./PointInterface";
|
import { PointInterface } from "./PointInterface";
|
||||||
import {
|
import {
|
||||||
CharacterLayerMessage,
|
CharacterLayerMessage,
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
PointMessage,
|
PointMessage,
|
||||||
PositionMessage
|
PositionMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import {CharacterLayer, ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
import { CharacterLayer, ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||||
import Direction = PositionMessage.Direction;
|
import Direction = PositionMessage.Direction;
|
||||||
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage";
|
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
|
|
||||||
export class ProtobufUtils {
|
export class ProtobufUtils {
|
||||||
|
|
||||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
public static toPositionMessage(point: PointInterface): PositionMessage {
|
||||||
let direction: Direction;
|
let direction: Direction;
|
||||||
switch (point.direction) {
|
switch (point.direction) {
|
||||||
case 'up':
|
case "up":
|
||||||
direction = Direction.UP;
|
direction = Direction.UP;
|
||||||
break;
|
break;
|
||||||
case 'down':
|
case "down":
|
||||||
direction = Direction.DOWN;
|
direction = Direction.DOWN;
|
||||||
break;
|
break;
|
||||||
case 'left':
|
case "left":
|
||||||
direction = Direction.LEFT;
|
direction = Direction.LEFT;
|
||||||
break;
|
break;
|
||||||
case 'right':
|
case "right":
|
||||||
direction = Direction.RIGHT;
|
direction = Direction.RIGHT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('unexpected direction');
|
throw new Error("unexpected direction");
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = new PositionMessage();
|
const position = new PositionMessage();
|
||||||
@ -44,16 +43,16 @@ export class ProtobufUtils {
|
|||||||
let direction: string;
|
let direction: string;
|
||||||
switch (position.getDirection()) {
|
switch (position.getDirection()) {
|
||||||
case Direction.UP:
|
case Direction.UP:
|
||||||
direction = 'up';
|
direction = "up";
|
||||||
break;
|
break;
|
||||||
case Direction.DOWN:
|
case Direction.DOWN:
|
||||||
direction = 'down';
|
direction = "down";
|
||||||
break;
|
break;
|
||||||
case Direction.LEFT:
|
case Direction.LEFT:
|
||||||
direction = 'left';
|
direction = "left";
|
||||||
break;
|
break;
|
||||||
case Direction.RIGHT:
|
case Direction.RIGHT:
|
||||||
direction = 'right';
|
direction = "right";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Unexpected direction");
|
throw new Error("Unexpected direction");
|
||||||
@ -82,7 +81,7 @@ export class ProtobufUtils {
|
|||||||
event: itemEventMessage.getEvent(),
|
event: itemEventMessage.getEvent(),
|
||||||
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
||||||
state: JSON.parse(itemEventMessage.getStatejson()),
|
state: JSON.parse(itemEventMessage.getStatejson()),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
||||||
@ -96,7 +95,7 @@ export class ProtobufUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
||||||
return characterLayers.map(function(characterLayer): CharacterLayerMessage {
|
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
||||||
const message = new CharacterLayerMessage();
|
const message = new CharacterLayerMessage();
|
||||||
message.setName(characterLayer.name);
|
message.setName(characterLayer.name);
|
||||||
if (characterLayer.url) {
|
if (characterLayer.url) {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isViewport =
|
export const isViewport = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
left: tg.isNumber,
|
left: tg.isNumber,
|
||||||
top: tg.isNumber,
|
top: tg.isNumber,
|
||||||
right: tg.isNumber,
|
right: tg.isNumber,
|
||||||
bottom: tg.isNumber,
|
bottom: tg.isNumber,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
export type ViewportInterface = tg.GuardedType<typeof isViewport>;
|
export type ViewportInterface = tg.GuardedType<typeof isViewport>;
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
import {ExSocketInterface} from "./Websocket/ExSocketInterface";
|
import { ExSocketInterface } from "./Websocket/ExSocketInterface";
|
||||||
import {apiClientRepository} from "../Services/ApiClientRepository";
|
import { apiClientRepository } from "../Services/ApiClientRepository";
|
||||||
import {
|
import {
|
||||||
BatchToPusherMessage,
|
BatchToPusherMessage,
|
||||||
CharacterLayerMessage, GroupLeftZoneMessage, GroupUpdateMessage, GroupUpdateZoneMessage,
|
CharacterLayerMessage,
|
||||||
PointMessage, PositionMessage, UserJoinedMessage,
|
GroupLeftZoneMessage,
|
||||||
UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage,
|
GroupUpdateMessage,
|
||||||
|
GroupUpdateZoneMessage,
|
||||||
|
PointMessage,
|
||||||
|
PositionMessage,
|
||||||
|
UserJoinedMessage,
|
||||||
|
UserJoinedZoneMessage,
|
||||||
|
UserLeftZoneMessage,
|
||||||
|
UserMovedMessage,
|
||||||
ZoneMessage,
|
ZoneMessage,
|
||||||
EmoteEventMessage,
|
EmoteEventMessage,
|
||||||
CompanionMessage
|
CompanionMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import {ClientReadableStream} from "grpc";
|
import { ClientReadableStream } from "grpc";
|
||||||
import {PositionDispatcher} from "_Model/PositionDispatcher";
|
import { PositionDispatcher } from "_Model/PositionDispatcher";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
|
|
||||||
const debug = Debug("zone");
|
const debug = Debug("zone");
|
||||||
@ -30,24 +37,38 @@ export type MovesCallback = (thing: Movable, position: PositionInterface, listen
|
|||||||
export type LeavesCallback = (thing: Movable, listener: User) => void;*/
|
export type LeavesCallback = (thing: Movable, listener: User) => void;*/
|
||||||
|
|
||||||
export class UserDescriptor {
|
export class UserDescriptor {
|
||||||
private constructor(public readonly userId: number, private name: string, private characterLayers: CharacterLayerMessage[], private position: PositionMessage, private visitCardUrl: string | null, private companion?: CompanionMessage) {
|
private constructor(
|
||||||
|
public readonly userId: number,
|
||||||
|
private name: string,
|
||||||
|
private characterLayers: CharacterLayerMessage[],
|
||||||
|
private position: PositionMessage,
|
||||||
|
private visitCardUrl: string | null,
|
||||||
|
private companion?: CompanionMessage
|
||||||
|
) {
|
||||||
if (!Number.isInteger(this.userId)) {
|
if (!Number.isInteger(this.userId)) {
|
||||||
throw new Error('UserDescriptor.userId is not an integer: '+this.userId);
|
throw new Error("UserDescriptor.userId is not an integer: " + this.userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createFromUserJoinedZoneMessage(message: UserJoinedZoneMessage): UserDescriptor {
|
public static createFromUserJoinedZoneMessage(message: UserJoinedZoneMessage): UserDescriptor {
|
||||||
const position = message.getPosition();
|
const position = message.getPosition();
|
||||||
if (position === undefined) {
|
if (position === undefined) {
|
||||||
throw new Error('Missing position');
|
throw new Error("Missing position");
|
||||||
}
|
}
|
||||||
return new UserDescriptor(message.getUserid(), message.getName(), message.getCharacterlayersList(), position, message.getVisitcardurl(), message.getCompanion());
|
return new UserDescriptor(
|
||||||
|
message.getUserid(),
|
||||||
|
message.getName(),
|
||||||
|
message.getCharacterlayersList(),
|
||||||
|
position,
|
||||||
|
message.getVisitcardurl(),
|
||||||
|
message.getCompanion()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(userMovedMessage: UserMovedMessage) {
|
public update(userMovedMessage: UserMovedMessage) {
|
||||||
const position = userMovedMessage.getPosition();
|
const position = userMovedMessage.getPosition();
|
||||||
if (position === undefined) {
|
if (position === undefined) {
|
||||||
throw new Error('Missing position');
|
throw new Error("Missing position");
|
||||||
}
|
}
|
||||||
this.position = position;
|
this.position = position;
|
||||||
}
|
}
|
||||||
@ -78,13 +99,12 @@ export class UserDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class GroupDescriptor {
|
export class GroupDescriptor {
|
||||||
private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) {
|
private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) {}
|
||||||
}
|
|
||||||
|
|
||||||
public static createFromGroupUpdateZoneMessage(message: GroupUpdateZoneMessage): GroupDescriptor {
|
public static createFromGroupUpdateZoneMessage(message: GroupUpdateZoneMessage): GroupDescriptor {
|
||||||
const position = message.getPosition();
|
const position = message.getPosition();
|
||||||
if (position === undefined) {
|
if (position === undefined) {
|
||||||
throw new Error('Missing position');
|
throw new Error("Missing position");
|
||||||
}
|
}
|
||||||
return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position);
|
return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position);
|
||||||
}
|
}
|
||||||
@ -97,7 +117,7 @@ export class GroupDescriptor {
|
|||||||
public toGroupUpdateMessage(): GroupUpdateMessage {
|
public toGroupUpdateMessage(): GroupUpdateMessage {
|
||||||
const groupUpdateMessage = new GroupUpdateMessage();
|
const groupUpdateMessage = new GroupUpdateMessage();
|
||||||
if (!Number.isInteger(this.groupId)) {
|
if (!Number.isInteger(this.groupId)) {
|
||||||
throw new Error('GroupDescriptor.groupId is not an integer: '+this.groupId);
|
throw new Error("GroupDescriptor.groupId is not an integer: " + this.groupId);
|
||||||
}
|
}
|
||||||
groupUpdateMessage.setGroupid(this.groupId);
|
groupUpdateMessage.setGroupid(this.groupId);
|
||||||
groupUpdateMessage.setGroupsize(this.groupSize);
|
groupUpdateMessage.setGroupsize(this.groupSize);
|
||||||
@ -108,8 +128,8 @@ export class GroupDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ZoneDescriptor {
|
interface ZoneDescriptor {
|
||||||
x: number,
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Zone {
|
export class Zone {
|
||||||
@ -120,21 +140,26 @@ export class Zone {
|
|||||||
private backConnection!: ClientReadableStream<BatchToPusherMessage>;
|
private backConnection!: ClientReadableStream<BatchToPusherMessage>;
|
||||||
private isClosing: boolean = false;
|
private isClosing: boolean = false;
|
||||||
|
|
||||||
constructor(private positionDispatcher: PositionDispatcher, private socketListener: ZoneEventListener, public readonly x: number, public readonly y: number, private onBackFailure: (e: Error|null, zone: Zone) => void) {
|
constructor(
|
||||||
}
|
private positionDispatcher: PositionDispatcher,
|
||||||
|
private socketListener: ZoneEventListener,
|
||||||
|
public readonly x: number,
|
||||||
|
public readonly y: number,
|
||||||
|
private onBackFailure: (e: Error | null, zone: Zone) => void
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a connection to the back server to track the users.
|
* Creates a connection to the back server to track the users.
|
||||||
*/
|
*/
|
||||||
public async init(): Promise<void> {
|
public async init(): Promise<void> {
|
||||||
debug('Opening connection to zone %d, %d on back server', this.x, this.y);
|
debug("Opening connection to zone %d, %d on back server", this.x, this.y);
|
||||||
const apiClient = await apiClientRepository.getClient(this.positionDispatcher.roomId);
|
const apiClient = await apiClientRepository.getClient(this.positionDispatcher.roomId);
|
||||||
const zoneMessage = new ZoneMessage();
|
const zoneMessage = new ZoneMessage();
|
||||||
zoneMessage.setRoomid(this.positionDispatcher.roomId);
|
zoneMessage.setRoomid(this.positionDispatcher.roomId);
|
||||||
zoneMessage.setX(this.x);
|
zoneMessage.setX(this.x);
|
||||||
zoneMessage.setY(this.y);
|
zoneMessage.setY(this.y);
|
||||||
this.backConnection = apiClient.listenZone(zoneMessage);
|
this.backConnection = apiClient.listenZone(zoneMessage);
|
||||||
this.backConnection.on('data', (batch: BatchToPusherMessage) => {
|
this.backConnection.on("data", (batch: BatchToPusherMessage) => {
|
||||||
for (const message of batch.getPayloadList()) {
|
for (const message of batch.getPayloadList()) {
|
||||||
if (message.hasUserjoinedzonemessage()) {
|
if (message.hasUserjoinedzonemessage()) {
|
||||||
const userJoinedZoneMessage = message.getUserjoinedzonemessage() as UserJoinedZoneMessage;
|
const userJoinedZoneMessage = message.getUserjoinedzonemessage() as UserJoinedZoneMessage;
|
||||||
@ -179,33 +204,32 @@ export class Zone {
|
|||||||
const userDescriptor = this.users.get(userId);
|
const userDescriptor = this.users.get(userId);
|
||||||
|
|
||||||
if (userDescriptor === undefined) {
|
if (userDescriptor === undefined) {
|
||||||
console.error('Unexpected move message received for user "'+userId+'"');
|
console.error('Unexpected move message received for user "' + userId + '"');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
userDescriptor.update(userMovedMessage);
|
userDescriptor.update(userMovedMessage);
|
||||||
|
|
||||||
this.notifyUserMove(userDescriptor);
|
this.notifyUserMove(userDescriptor);
|
||||||
} else if(message.hasEmoteeventmessage()) {
|
} else if (message.hasEmoteeventmessage()) {
|
||||||
const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage;
|
const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage;
|
||||||
this.notifyEmote(emoteEventMessage);
|
this.notifyEmote(emoteEventMessage);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unexpected message');
|
throw new Error("Unexpected message");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.backConnection.on('error', (e) => {
|
this.backConnection.on("error", (e) => {
|
||||||
if (!this.isClosing) {
|
if (!this.isClosing) {
|
||||||
debug('Error on back connection')
|
debug("Error on back connection");
|
||||||
this.close();
|
this.close();
|
||||||
this.onBackFailure(e, this);
|
this.onBackFailure(e, this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.backConnection.on('close', () => {
|
this.backConnection.on("close", () => {
|
||||||
if (!this.isClosing) {
|
if (!this.isClosing) {
|
||||||
debug('Close on back connection')
|
debug("Close on back connection");
|
||||||
this.close();
|
this.close();
|
||||||
this.onBackFailure(null, this);
|
this.onBackFailure(null, this);
|
||||||
}
|
}
|
||||||
@ -213,7 +237,7 @@ export class Zone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public close(): void {
|
public close(): void {
|
||||||
debug('Closing connection to zone %d, %d on back server', this.x, this.y);
|
debug("Closing connection to zone %d, %d on back server", this.x, this.y);
|
||||||
this.isClosing = true;
|
this.isClosing = true;
|
||||||
this.backConnection.cancel();
|
this.backConnection.cancel();
|
||||||
}
|
}
|
||||||
@ -225,7 +249,7 @@ export class Zone {
|
|||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this user entered
|
* Notify listeners of this zone that this user entered
|
||||||
*/
|
*/
|
||||||
private notifyUserEnter(user: UserDescriptor, oldZone: ZoneDescriptor|undefined) {
|
private notifyUserEnter(user: UserDescriptor, oldZone: ZoneDescriptor | undefined) {
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
if (listener.userId === user.userId) {
|
if (listener.userId === user.userId) {
|
||||||
continue;
|
continue;
|
||||||
@ -241,7 +265,7 @@ export class Zone {
|
|||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this group entered
|
* Notify listeners of this zone that this group entered
|
||||||
*/
|
*/
|
||||||
private notifyGroupEnter(group: GroupDescriptor, oldZone: ZoneDescriptor|undefined) {
|
private notifyGroupEnter(group: GroupDescriptor, oldZone: ZoneDescriptor | undefined) {
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
if (oldZone === undefined || !this.isListeningZone(listener, oldZone.x, oldZone.y)) {
|
if (oldZone === undefined || !this.isListeningZone(listener, oldZone.x, oldZone.y)) {
|
||||||
this.socketListener.onGroupEnters(group, listener);
|
this.socketListener.onGroupEnters(group, listener);
|
||||||
@ -254,7 +278,7 @@ export class Zone {
|
|||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this user left
|
* Notify listeners of this zone that this user left
|
||||||
*/
|
*/
|
||||||
private notifyUserLeft(userId: number, newZone: ZoneDescriptor|undefined) {
|
private notifyUserLeft(userId: number, newZone: ZoneDescriptor | undefined) {
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
if (listener.userId === userId) {
|
if (listener.userId === userId) {
|
||||||
continue;
|
continue;
|
||||||
@ -279,7 +303,7 @@ export class Zone {
|
|||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this group left
|
* Notify listeners of this zone that this group left
|
||||||
*/
|
*/
|
||||||
private notifyGroupLeft(groupId: number, newZone: ZoneDescriptor|undefined) {
|
private notifyGroupLeft(groupId: number, newZone: ZoneDescriptor | undefined) {
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
if (listener.groupId === groupId) {
|
if (listener.groupId === groupId) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { App as _App, AppOptions } from 'uWebSockets.js';
|
import { App as _App, AppOptions } from "uWebSockets.js";
|
||||||
import BaseApp from './baseapp';
|
import BaseApp from "./baseapp";
|
||||||
import { extend } from './utils';
|
import { extend } from "./utils";
|
||||||
import { UwsApp } from './types';
|
import { UwsApp } from "./types";
|
||||||
|
|
||||||
class App extends (<UwsApp>_App) {
|
class App extends (<UwsApp>_App) {
|
||||||
constructor(options: AppOptions = {}) {
|
constructor(options: AppOptions = {}) {
|
||||||
super(options); // eslint-disable-line constructor-super
|
super(options); // eslint-disable-line constructor-super
|
||||||
extend(this, new BaseApp());
|
extend(this, new BaseApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -1,116 +1,109 @@
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from "stream";
|
||||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||||
|
|
||||||
import formData from './formdata';
|
import formData from "./formdata";
|
||||||
import { stob } from './utils';
|
import { stob } from "./utils";
|
||||||
import { Handler } from './types';
|
import { Handler } from "./types";
|
||||||
import {join} from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
|
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
|
||||||
const noOp = () => true;
|
const noOp = () => true;
|
||||||
|
|
||||||
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
||||||
const contType = req.getHeader('content-type');
|
const contType = req.getHeader("content-type");
|
||||||
|
|
||||||
res.bodyStream = function() {
|
res.bodyStream = function () {
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
||||||
|
|
||||||
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
||||||
// uint and then slicing is bit faster than slice and then uint
|
// uint and then slicing is bit faster than slice and then uint
|
||||||
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
stream.push(null);
|
stream.push(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
res.body = () => stob(res.bodyStream());
|
res.body = () => stob(res.bodyStream());
|
||||||
|
|
||||||
if (contType.includes('application/json'))
|
if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
|
||||||
res.json = async () => JSON.parse(await res.body());
|
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
|
||||||
if (contTypes.map(t => contType.includes(t)).includes(true))
|
|
||||||
res.formData = formData.bind(res, contType);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class BaseApp {
|
class BaseApp {
|
||||||
_sockets = new Map();
|
_sockets = new Map();
|
||||||
ws!: TemplatedApp['ws'];
|
ws!: TemplatedApp["ws"];
|
||||||
get!: TemplatedApp['get'];
|
get!: TemplatedApp["get"];
|
||||||
_post!: TemplatedApp['post'];
|
_post!: TemplatedApp["post"];
|
||||||
_put!: TemplatedApp['put'];
|
_put!: TemplatedApp["put"];
|
||||||
_patch!: TemplatedApp['patch'];
|
_patch!: TemplatedApp["patch"];
|
||||||
_listen!: TemplatedApp['listen'];
|
_listen!: TemplatedApp["listen"];
|
||||||
|
|
||||||
post(pattern: string, handler: Handler) {
|
post(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._post(pattern, (res, req) => {
|
||||||
this._post(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
handler(res, req);
|
||||||
handler(res, req);
|
});
|
||||||
});
|
return this;
|
||||||
return this;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
put(pattern: string, handler: Handler) {
|
put(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._put(pattern, (res, req) => {
|
||||||
this._put(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
|
||||||
|
|
||||||
handler(res, req);
|
handler(res, req);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
patch(pattern: string, handler: Handler) {
|
patch(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._patch(pattern, (res, req) => {
|
||||||
this._patch(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
|
||||||
|
|
||||||
handler(res, req);
|
handler(res, req);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
||||||
if (typeof p === 'number' && typeof h === 'string') {
|
if (typeof p === "number" && typeof h === "string") {
|
||||||
this._listen(h, p, socket => {
|
this._listen(h, p, (socket) => {
|
||||||
this._sockets.set(p, socket);
|
this._sockets.set(p, socket);
|
||||||
if (cb === undefined) {
|
if (cb === undefined) {
|
||||||
throw new Error('cb undefined');
|
throw new Error("cb undefined");
|
||||||
|
}
|
||||||
|
cb(socket);
|
||||||
|
});
|
||||||
|
} else if (typeof h === "number" && typeof p === "function") {
|
||||||
|
this._listen(h, (socket) => {
|
||||||
|
this._sockets.set(h, socket);
|
||||||
|
p(socket);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
|
||||||
}
|
}
|
||||||
cb(socket);
|
|
||||||
});
|
return this;
|
||||||
} else if (typeof h === 'number' && typeof p === 'function') {
|
|
||||||
this._listen(h, socket => {
|
|
||||||
this._sockets.set(h, socket);
|
|
||||||
p(socket);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw Error(
|
|
||||||
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
close(port: null | number = null) {
|
||||||
}
|
if (port) {
|
||||||
|
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
||||||
close(port: null | number = null) {
|
this._sockets.delete(port);
|
||||||
if (port) {
|
} else {
|
||||||
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
this._sockets.forEach((app) => {
|
||||||
this._sockets.delete(port);
|
us_listen_socket_close(app);
|
||||||
} else {
|
});
|
||||||
this._sockets.forEach(app => {
|
this._sockets.clear();
|
||||||
us_listen_socket_close(app);
|
}
|
||||||
});
|
return this;
|
||||||
this._sockets.clear();
|
|
||||||
}
|
}
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BaseApp;
|
export default BaseApp;
|
||||||
|
@ -1,100 +1,99 @@
|
|||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from "fs";
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from "path";
|
||||||
import Busboy from 'busboy';
|
import Busboy from "busboy";
|
||||||
import mkdirp from 'mkdirp';
|
import mkdirp from "mkdirp";
|
||||||
|
|
||||||
function formData(
|
function formData(
|
||||||
contType: string,
|
contType: string,
|
||||||
options: busboy.BusboyConfig & {
|
options: busboy.BusboyConfig & {
|
||||||
abortOnLimit?: boolean;
|
abortOnLimit?: boolean;
|
||||||
tmpDir?: string;
|
tmpDir?: string;
|
||||||
onFile?: (
|
onFile?: (
|
||||||
fieldname: string,
|
fieldname: string,
|
||||||
file: NodeJS.ReadableStream,
|
file: NodeJS.ReadableStream,
|
||||||
filename: string,
|
filename: string,
|
||||||
encoding: string,
|
encoding: string,
|
||||||
mimetype: string
|
mimetype: string
|
||||||
) => string;
|
) => string;
|
||||||
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
filename?: (oldName: string) => string;
|
filename?: (oldName: string) => string;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
console.log('Enter form data');
|
console.log("Enter form data");
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'content-type': contType
|
"content-type": contType,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const busb = new Busboy(options);
|
const busb = new Busboy(options);
|
||||||
const ret = {};
|
const ret = {};
|
||||||
|
|
||||||
this.bodyStream().pipe(busb);
|
this.bodyStream().pipe(busb);
|
||||||
|
|
||||||
busb.on('limit', () => {
|
busb.on("limit", () => {
|
||||||
if (options.abortOnLimit) {
|
if (options.abortOnLimit) {
|
||||||
reject(Error('limit'));
|
reject(Error("limit"));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
|
||||||
|
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
|
||||||
|
filename,
|
||||||
|
encoding,
|
||||||
|
mimetype,
|
||||||
|
filePath: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof options.tmpDir === "string") {
|
||||||
|
if (typeof options.filename === "function") filename = options.filename(filename);
|
||||||
|
const fileToSave = join(options.tmpDir, filename);
|
||||||
|
mkdirp(dirname(fileToSave));
|
||||||
|
|
||||||
|
file.pipe(createWriteStream(fileToSave));
|
||||||
|
value.filePath = fileToSave;
|
||||||
|
}
|
||||||
|
if (typeof options.onFile === "function") {
|
||||||
|
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRetValue(ret, fieldname, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("field", function (fieldname, value) {
|
||||||
|
if (typeof options.onField === "function") options.onField(fieldname, value);
|
||||||
|
|
||||||
|
setRetValue(ret, fieldname, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("finish", function () {
|
||||||
|
resolve(ret);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("error", reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
|
||||||
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = {
|
|
||||||
filename,
|
|
||||||
encoding,
|
|
||||||
mimetype,
|
|
||||||
filePath: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof options.tmpDir === 'string') {
|
|
||||||
if (typeof options.filename === 'function') filename = options.filename(filename);
|
|
||||||
const fileToSave = join(options.tmpDir, filename);
|
|
||||||
mkdirp(dirname(fileToSave));
|
|
||||||
|
|
||||||
file.pipe(createWriteStream(fileToSave));
|
|
||||||
value.filePath = fileToSave;
|
|
||||||
}
|
|
||||||
if (typeof options.onFile === 'function') {
|
|
||||||
value.filePath =
|
|
||||||
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRetValue(ret, fieldname, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('field', function(fieldname, value) {
|
|
||||||
if (typeof options.onField === 'function') options.onField(fieldname, value);
|
|
||||||
|
|
||||||
setRetValue(ret, fieldname, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('finish', function() {
|
|
||||||
resolve(ret);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRetValue(
|
function setRetValue(
|
||||||
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
fieldname: string,
|
fieldname: string,
|
||||||
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
) {
|
) {
|
||||||
if (fieldname.endsWith('[]')) {
|
if (fieldname.endsWith("[]")) {
|
||||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
fieldname = fieldname.slice(0, fieldname.length - 2);
|
||||||
if (Array.isArray(ret[fieldname])) {
|
if (Array.isArray(ret[fieldname])) {
|
||||||
ret[fieldname].push(value);
|
ret[fieldname].push(value);
|
||||||
|
} else {
|
||||||
|
ret[fieldname] = [value];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ret[fieldname] = [value];
|
if (Array.isArray(ret[fieldname])) {
|
||||||
|
ret[fieldname].push(value);
|
||||||
|
} else if (ret[fieldname]) {
|
||||||
|
ret[fieldname] = [ret[fieldname], value];
|
||||||
|
} else {
|
||||||
|
ret[fieldname] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (Array.isArray(ret[fieldname])) {
|
|
||||||
ret[fieldname].push(value);
|
|
||||||
} else if (ret[fieldname]) {
|
|
||||||
ret[fieldname] = [ret[fieldname], value];
|
|
||||||
} else {
|
|
||||||
ret[fieldname] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default formData;
|
export default formData;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js';
|
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
|
||||||
import BaseApp from './baseapp';
|
import BaseApp from "./baseapp";
|
||||||
import { extend } from './utils';
|
import { extend } from "./utils";
|
||||||
import { UwsApp } from './types';
|
import { UwsApp } from "./types";
|
||||||
|
|
||||||
class SSLApp extends (<UwsApp>_SSLApp) {
|
class SSLApp extends (<UwsApp>_SSLApp) {
|
||||||
constructor(options: AppOptions) {
|
constructor(options: AppOptions) {
|
||||||
super(options); // eslint-disable-line constructor-super
|
super(options); // eslint-disable-line constructor-super
|
||||||
extend(this, new BaseApp());
|
extend(this, new BaseApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SSLApp;
|
export default SSLApp;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user