Merge branch 'develop' of github.com:thecodingmachine/workadventure into main

This commit is contained in:
_Bastler 2021-06-24 13:02:41 +02:00
commit f4d2897957
112 changed files with 2860 additions and 2505 deletions

View File

@ -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"

View File

@ -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.

View File

@ -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}'"

View File

@ -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", "*");
} }
} }

View File

@ -1,7 +1,7 @@
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";
@ -10,26 +10,27 @@ export class DebugController {
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") {
return "Listeners";
} }
if (key === 'socket') { if (key === "socket") {
return 'Socket'; return "Socket";
} }
if (key === 'batchedMessages') { if (key === "batchedMessages") {
return 'BatchedMessages'; return "BatchedMessages";
} }
if (value instanceof Map) { if (value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
@ -46,8 +47,8 @@ export class DebugController {
} else { } else {
return value; return value;
} }
} })
)); );
}); });
} }
} }

View File

@ -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());
} }
} }

View File

@ -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,
} };

View File

@ -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();

View File

@ -39,12 +39,13 @@ 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(
roomId: string,
connectCallback: ConnectCallback, connectCallback: ConnectCallback,
disconnectCallback: DisconnectCallback, disconnectCallback: DisconnectCallback,
minDistance: number, minDistance: number,
@ -52,7 +53,7 @@ export class GameRoom {
onEnters: EntersCallback, onEnters: EntersCallback,
onMoves: MovesCallback, onMoves: MovesCallback,
onLeaves: LeavesCallback, onLeaves: LeavesCallback,
onEmote: EmoteCallback, onEmote: EmoteCallback
) { ) {
this.roomId = roomId; this.roomId = roomId;
@ -65,7 +66,6 @@ export class GameRoom {
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>();
@ -96,11 +96,12 @@ export class GameRoom {
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,
@ -129,9 +130,9 @@ export class GameRoom {
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);
@ -181,14 +182,16 @@ export class GameRoom {
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,13 +252,12 @@ 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) {
@ -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));
} }
@ -327,7 +331,7 @@ export class GameRoom {
} }
public incrementVersion(): number { public incrementVersion(): number {
this.versionNumber++ this.versionNumber++;
return this.versionNumber; return this.versionNumber;
} }

View File

@ -7,7 +7,6 @@ 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;
@ -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,
}; };
} }
@ -95,16 +99,14 @@ 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);
@ -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);

View File

@ -4,5 +4,5 @@ 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;
} }

View File

@ -1,4 +1,4 @@
export interface PositionInterface { export interface PositionInterface {
x: number, x: number;
y: number y: number;
} }

View File

@ -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);
} }
} }

View File

@ -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 };
} };

View File

@ -4,7 +4,13 @@ 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 {
BatchMessage,
CompanionMessage,
PusherToBackMessage,
ServerToClientMessage,
SubMessage,
} from "../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>; export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
@ -42,7 +48,6 @@ 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;

View File

@ -1,4 +1,4 @@
export interface CharacterLayer { export interface CharacterLayer {
name: string, name: string;
url: string|undefined url: string | undefined;
} }

View File

@ -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>;

View File

@ -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
) {}
} }
}

View File

@ -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>;

View File

@ -3,7 +3,7 @@ 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;
@ -11,24 +11,23 @@ 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 {

View File

@ -14,8 +14,14 @@ 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
@ -24,12 +30,23 @@ export class Zone {
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);
} }
@ -57,7 +74,6 @@ export class Zone {
} }
} }
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);
@ -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);
} }
} }
} }

View File

@ -11,13 +11,15 @@ 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";
@ -27,23 +29,25 @@ 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
.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
.then(({ room: gameRoom, user: myUser }) => {
if (call.writable) { if (call.writable) {
room = gameRoom; room = gameRoom;
user = myUser; user = myUser;
@ -53,27 +57,46 @@ const roomManager: IRoomManagerServer = {
} }
}); });
} 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(
user,
message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
);
} else if (message.hasEmotepromptmessage()) { } else if (message.hasEmotepromptmessage()) {
socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage); socketManager.handleEmoteEventMessage(
room,
user,
message.getEmotepromptmessage() as EmotePromptMessage
);
} else if (message.hasSendusermessage()) { } else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage(); const sendUserMessage = message.getSendusermessage();
if (sendUserMessage !== undefined) { if (sendUserMessage !== undefined) {
@ -85,18 +108,17 @@ const roomManager: IRoomManagerServer = {
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,11 +215,17 @@ 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());
}, },

View File

@ -1,7 +1,7 @@
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 = {}) {

View File

@ -1,16 +1,16 @@
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();
@ -29,24 +29,21 @@ const handleBody = (res: HttpResponse, req: HttpRequest) => {
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);
@ -55,8 +52,7 @@ class BaseApp {
} }
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);
@ -66,8 +62,7 @@ class BaseApp {
} }
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);
@ -77,23 +72,21 @@ class BaseApp {
} }
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); cb(socket);
}); });
} else if (typeof h === 'number' && typeof p === 'function') { } else if (typeof h === "number" && typeof p === "function") {
this._listen(h, socket => { this._listen(h, (socket) => {
this._sockets.set(h, socket); this._sockets.set(h, socket);
p(socket); p(socket);
}); });
} else { } else {
throw Error( throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
);
} }
return this; return this;
@ -104,7 +97,7 @@ class BaseApp {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
this._sockets.delete(port); this._sockets.delete(port);
} else { } else {
this._sockets.forEach(app => { this._sockets.forEach((app) => {
us_listen_socket_close(app); us_listen_socket_close(app);
}); });
this._sockets.clear(); this._sockets.clear();

View File

@ -1,7 +1,7 @@
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,
@ -19,9 +19,9 @@ function formData(
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) => {
@ -30,47 +30,46 @@ function formData(
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) { busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = { const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
filename, filename,
encoding, encoding,
mimetype, mimetype,
filePath: undefined filePath: undefined,
}; };
if (typeof options.tmpDir === 'string') { if (typeof options.tmpDir === "string") {
if (typeof options.filename === 'function') filename = options.filename(filename); if (typeof options.filename === "function") filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename); const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave)); mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave)); file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave; value.filePath = fileToSave;
} }
if (typeof options.onFile === 'function') { if (typeof options.onFile === "function") {
value.filePath = value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
} }
setRetValue(ret, fieldname, value); setRetValue(ret, fieldname, value);
}); });
busb.on('field', function(fieldname, value) { busb.on("field", function (fieldname, value) {
if (typeof options.onField === 'function') options.onField(fieldname, value); if (typeof options.onField === "function") options.onField(fieldname, value);
setRetValue(ret, fieldname, value); setRetValue(ret, fieldname, value);
}); });
busb.on('finish', function() { busb.on("finish", function () {
resolve(ret); resolve(ret);
}); });
busb.on('error', reject); busb.on("error", reject);
}); });
} }
@ -79,7 +78,7 @@ function setRetValue(
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);

View File

@ -1,7 +1,7 @@
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) {

View File

@ -1,4 +1,4 @@
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;

View File

@ -1,25 +1,24 @@
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));

View File

@ -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,
}; };

View File

@ -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;
} };

View File

@ -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 {

View File

@ -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;

View File

@ -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,12 +54,12 @@ 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 });
} }
} }

View File

@ -27,7 +27,9 @@ 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";
@ -38,7 +40,7 @@ import {
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";
@ -52,15 +54,13 @@ 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,14 +124,13 @@ 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));
//room.setViewport(client, client.viewport); //room.setViewport(client, client.viewport);
@ -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);
} }
@ -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);
} }
@ -249,13 +256,13 @@ 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,
@ -263,19 +270,25 @@ export class SocketManager {
(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,7 +297,7 @@ 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 };
} }
@ -292,7 +305,7 @@ export class SocketManager {
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,7 +325,7 @@ 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.");
} }
} }
@ -331,7 +344,7 @@ 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.");
} }
} }
@ -341,11 +354,10 @@ export class SocketManager {
} 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);
@ -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);
} }
@ -434,8 +445,8 @@ export class SocketManager {
webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setName(user.name);
webrtcStartMessage2.setInitiator(false); webrtcStartMessage2.setInitiator(false);
if (TURN_STATIC_AUTH_SECRET !== '') { 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);
} }
@ -447,7 +458,6 @@ export class SocketManager {
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,
}; };
} }
@ -492,7 +502,6 @@ export class SocketManager {
otherUser.socket.write(serverToClientMessage1); otherUser.socket.write(serverToClientMessage1);
//} //}
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
webrtcDisconnectMessage2.setUserid(otherUser.id); webrtcDisconnectMessage2.setUserid(otherUser.id);
@ -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,
"sub": JITSI_URL,
"room": room,
"moderator": isAdmin
}, SECRET_JITSI_KEY, {
expiresIn: '1d',
algorithm: "HS256",
header:
{ {
"alg": "HS256", aud: "jitsi",
"typ": "JWT" iss: JITSI_ISS,
sub: JITSI_URL,
room: room,
moderator: isAdmin,
},
SECRET_JITSI_KEY,
{
expiresIn: "1d",
algorithm: "HS256",
header: {
alg: "HS256",
typ: "JWT",
},
} }
}); );
const sendJitsiJwtMessage = new SendJitsiJwtMessage(); const sendJitsiJwtMessage = new SendJitsiJwtMessage();
sendJitsiJwtMessage.setJitsiroom(room); sendJitsiJwtMessage.setJitsiroom(room);
@ -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,11 +761,15 @@ 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;
} }
@ -750,7 +783,7 @@ 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;
@ -759,8 +792,8 @@ export class SocketManager {
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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

View File

@ -67,8 +67,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}'"
}, },

View File

@ -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>

View File

@ -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 nes-btn is-dark" 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-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}> <div class="btn-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}>
{#if $requestedMicrophoneState} {#if $requestedMicrophoneState}
<img src={microphoneImg} alt="Turn on microphone"> <img src={microphoneImg} alt="Turn on microphone">

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

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

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View 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
}
}
}
}

View 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

View 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

View File

@ -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();
} }
@ -1077,23 +1071,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) {
@ -1206,7 +1183,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() {
@ -1353,7 +1330,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);
@ -1583,20 +1560,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;
@ -1606,10 +1580,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');
@ -1696,6 +1666,6 @@ ${escapedMessage}
zoomByFactor(zoomFactor: number) { zoomByFactor(zoomFactor: number) {
waScaleManager.zoomModifier *= zoomFactor; waScaleManager.zoomModifier *= zoomFactor;
this.updateCameraOffset(); biggestAvailableAreaStore.recompute();
} }
} }

View File

@ -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', () => {

View 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();

View 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();

View File

@ -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
*/ */

View File

@ -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 => { update(users => {
users.set(user.userId, user); users.set(peer.userId, peer);
return users; return users;
}); });
}
},
onDisconnect(userId: number) {
update(users => {
users.delete(userId);
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();

View 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();

View File

@ -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();
};
})

View File

@ -0,0 +1,3 @@
import {writable} from "svelte/store";
export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null);

View 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();

View 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();

View 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);
};
});

View File

@ -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);
} }

View File

@ -16,14 +16,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';
@ -35,294 +27,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.classList.add("nes-container", "is-rounded", "is-dark");
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);

View File

@ -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>();
//FIX ME SOUNDMETER: check stalability of sound meter calculation
//mySoundMeterElement: HTMLDivElement;
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>(); stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
showReportModalCallBacks: Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
@ -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,32 +160,10 @@ 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 {
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){ 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() {
return discussionManager.activatedDiscussion;
}
public setUserInputManager(userInputManager : UserInputManager){ 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") {

View File

@ -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);

View File

@ -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")

View File

@ -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);

View File

@ -39,21 +39,20 @@ body .message-info.warning {
.video-container { .video-container {
position: relative; position: relative;
transition: all 0.2s ease; transition: all 0.2s ease;
background-color: #00000099;
video {
width: 100%;
height: 100%; height: 100%;
max-height: 90vh;
} }
.video-container.nes-container.is-dark { i {
padding: 12px 12px !important;
}
.video-container i {
position: absolute; position: absolute;
width: 100px; width: 100px;
height: 100px; height: 100px;
left: calc(50% - 50px); left: calc(50% - 50px);
top: calc(50% - 50px); top: calc(50% - 50px);
background-color: black;
border-radius: 50%;
text-align: center; text-align: center;
padding-top: 32px; padding-top: 32px;
font-size: 28px; font-size: 28px;
@ -61,7 +60,7 @@ body .message-info.warning {
overflow: hidden; overflow: hidden;
} }
.video-container img { img {
position: absolute; position: absolute;
display: none; display: none;
width: 40px; width: 40px;
@ -72,20 +71,15 @@ body .message-info.warning {
z-index: 2; z-index: 2;
} }
.video-container img.block-logo { img.block-logo {
left: 30%; left: 30%;
bottom: 15%; bottom: 15%;
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.video-container button.report { button.report{
display: block; display: block;
background: none;
background-color: rgba(0, 0, 0, 0);
border: none;
background-color: black;
border-radius: 15px;
position: absolute; position: absolute;
width: 0px; width: 0px;
height: 35px; height: 35px;
@ -95,18 +89,8 @@ body .message-info.warning {
overflow: hidden; overflow: hidden;
z-index: 2; z-index: 2;
transition: all .5s ease; transition: all .5s ease;
}
.video-container:hover button.report { img{
width: 35px;
padding: 10px;
}
.video-container button.report:hover {
width: 160px;
}
.video-container button.report img {
position: absolute; position: absolute;
display: block; display: block;
bottom: 5px; bottom: 5px;
@ -117,7 +101,7 @@ body .message-info.warning {
height: 25px; height: 25px;
} }
.video-container button.report span { span {
position: absolute; position: absolute;
bottom: 6px; bottom: 6px;
left: 36px; left: 36px;
@ -125,17 +109,24 @@ body .message-info.warning {
font-size: 16px; font-size: 16px;
} }
.video-container img.active { img.active {
display: block !important; display: block !important;
} }
.video-container video {
height: 100%;
} }
.video-container video:focus { &:hover button.report{
width: 35px;
padding: 10px;
&:hover {
width: 160px;
}
}
video:focus{
outline: none; outline: none;
} }
}
.video-container.div-myCamVideo{ .video-container.div-myCamVideo{
border: none; border: none;
@ -213,7 +204,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;
@ -230,8 +221,7 @@ video.myCamVideo{
justify-content: center; justify-content: center;
width: 44px; width: 44px;
height: 44px; height: 44px;
width: auto; transform: translateY(15px);
transform: translateY(20px);
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
margin: 0 4%; margin: 0 4%;
} }
@ -277,6 +267,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;

View File

@ -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);

View File

@ -2,11 +2,13 @@ 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;
@ -23,21 +25,21 @@ 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;
@ -57,9 +59,7 @@ export class AdminController extends BaseController{
} }
res.writeStatus("200"); res.writeStatus("200");
res.end('ok'); res.end("ok");
}); });
} }
@ -71,39 +71,38 @@ 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(
targets.map((roomId) => {
return apiClientRepository.getClient(roomId).then((roomClient) => { return apiClientRepository.getClient(roomId).then((roomClient) => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
if (type === 'message') { if (type === "message") {
const roomMessage = new AdminRoomMessage(); const roomMessage = new AdminRoomMessage();
roomMessage.setMessage(text); roomMessage.setMessage(text);
roomMessage.setRoomid(roomId); roomMessage.setRoomid(roomId);
@ -111,7 +110,7 @@ export class AdminController extends BaseController{
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);
@ -119,11 +118,10 @@ export class AdminController extends BaseController{
err ? rej(err) : res(); 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");
}); });
} }
} }

View File

@ -1,4 +1,4 @@
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";
@ -6,11 +6,10 @@ 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();
@ -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,7 +48,8 @@ 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(
JSON.stringify({
authToken, authToken,
userUuid, userUuid,
organizationSlug, organizationSlug,
@ -57,17 +57,14 @@ export class AuthenticateController extends BaseController {
roomSlug, roomSlug,
mapUrlStart, mapUrlStart,
organizationMemberToken, organizationMemberToken,
textures textures,
})); })
);
} catch (e) { } catch (e) {
this.errorToResponse(e, res); this.errorToResponse(e, res);
} }
})(); })();
}); });
} }
private verify() { private verify() {
@ -82,28 +79,31 @@ 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.
@ -115,17 +115,19 @@ export class AuthenticateController extends BaseController {
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(
JSON.stringify({
authToken, authToken,
userUuid, userUuid,
})); })
);
}); });
} }
} }

View File

@ -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,12 +15,12 @@ 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) {
@ -32,7 +31,7 @@ export class BaseController {
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");
} }

View File

@ -2,7 +2,7 @@ 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";
@ -11,18 +11,19 @@ export class DebugController {
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")
.end(
stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
if (value instanceof Map) { if (value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) { for (const [mapKey, mapValue] of value.entries()) {
@ -38,8 +39,8 @@ export class DebugController {
} else { } else {
return value; return value;
} }
} })
)); );
}); });
} }
} }

View File

@ -19,7 +19,7 @@ import {
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";
@ -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,16 +71,26 @@ 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) {
@ -91,7 +98,7 @@ export class IoSocketController {
} }
}, },
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,
@ -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;
@ -145,22 +152,22 @@ export class IoSocketController {
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];
} }
@ -179,25 +186,32 @@ export class IoSocketController {
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(
'Cannot find user with uuid "' +
userUuid +
'". Performing an anonymous login instead.'
);
} else if (err?.response?.status == 403) { } 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, rejected: true,
message: err?.response?.data.message, message: err?.response?.data.message,
status: err?.response?.status status: err?.response?.status,
}, websocketKey, },
websocketKey,
websocketProtocol, websocketProtocol,
websocketExtensions, websocketExtensions,
context); context
);
} else { } 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,13 +293,16 @@ 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, rejected: true,
message: e.message ? e.message : '500 Internal Server Error' message: e.message ? e.message : "500 Internal Server Error",
}, websocketKey, },
websocketKey,
websocketProtocol, websocketProtocol,
websocketExtensions, websocketExtensions,
context); context
);
} }
})(); })();
}, },
@ -283,7 +310,7 @@ export class IoSocketController {
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);
@ -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(
client,
message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
);
} else if (message.hasEmotepromptmessage()) { } else if (message.hasEmotepromptmessage()) {
socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage); 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,8 +400,8 @@ 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
@ -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;

View File

@ -3,16 +3,13 @@ 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);
} }
})(); })();
}); });
} }
} }

View File

@ -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());
} }
} }

View File

@ -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,
} };

View File

@ -4,5 +4,5 @@ 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;
} }

View File

@ -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);

View File

@ -1,4 +1,4 @@
export interface PositionInterface { export interface PositionInterface {
x: number, x: number;
y: number y: number;
} }

View File

@ -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;

View File

@ -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 };
} };

View File

@ -4,11 +4,12 @@ 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";
@ -16,6 +17,6 @@ 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;
} }

View File

@ -6,9 +6,9 @@ import {
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";
@ -16,8 +16,8 @@ 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 {
@ -37,11 +37,11 @@ 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>;
} }

View File

@ -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>;

View File

@ -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
) {}
} }

View File

@ -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>;

View File

@ -3,7 +3,7 @@ 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;
@ -11,24 +11,23 @@ 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 {

View File

@ -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>;

View File

@ -2,12 +2,19 @@ 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";
@ -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;
@ -190,22 +215,21 @@ export class Zone {
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();
} }

View File

@ -1,7 +1,7 @@
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 = {}) {

View File

@ -1,16 +1,16 @@
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();
@ -29,24 +29,21 @@ const handleBody = (res: HttpResponse, req: HttpRequest) => {
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);
@ -55,8 +52,7 @@ class BaseApp {
} }
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);
@ -66,8 +62,7 @@ class BaseApp {
} }
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);
@ -77,23 +72,21 @@ class BaseApp {
} }
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); cb(socket);
}); });
} else if (typeof h === 'number' && typeof p === 'function') { } else if (typeof h === "number" && typeof p === "function") {
this._listen(h, socket => { this._listen(h, (socket) => {
this._sockets.set(h, socket); this._sockets.set(h, socket);
p(socket); p(socket);
}); });
} else { } else {
throw Error( throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
);
} }
return this; return this;
@ -104,7 +97,7 @@ class BaseApp {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
this._sockets.delete(port); this._sockets.delete(port);
} else { } else {
this._sockets.forEach(app => { this._sockets.forEach((app) => {
us_listen_socket_close(app); us_listen_socket_close(app);
}); });
this._sockets.clear(); this._sockets.clear();

View File

@ -1,7 +1,7 @@
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,
@ -19,9 +19,9 @@ function formData(
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) => {
@ -30,47 +30,46 @@ function formData(
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) { busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = { const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
filename, filename,
encoding, encoding,
mimetype, mimetype,
filePath: undefined filePath: undefined,
}; };
if (typeof options.tmpDir === 'string') { if (typeof options.tmpDir === "string") {
if (typeof options.filename === 'function') filename = options.filename(filename); if (typeof options.filename === "function") filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename); const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave)); mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave)); file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave; value.filePath = fileToSave;
} }
if (typeof options.onFile === 'function') { if (typeof options.onFile === "function") {
value.filePath = value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
} }
setRetValue(ret, fieldname, value); setRetValue(ret, fieldname, value);
}); });
busb.on('field', function(fieldname, value) { busb.on("field", function (fieldname, value) {
if (typeof options.onField === 'function') options.onField(fieldname, value); if (typeof options.onField === "function") options.onField(fieldname, value);
setRetValue(ret, fieldname, value); setRetValue(ret, fieldname, value);
}); });
busb.on('finish', function() { busb.on("finish", function () {
resolve(ret); resolve(ret);
}); });
busb.on('error', reject); busb.on("error", reject);
}); });
} }
@ -79,7 +78,7 @@ function setRetValue(
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);

View File

@ -1,7 +1,7 @@
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) {

View File

@ -1,4 +1,4 @@
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;

View File

@ -1,25 +1,24 @@
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));

View File

@ -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,
}; };

Some files were not shown because too many files have changed in this diff Show More