Merge pull request #1226 from thecodingmachine/apply_prettier_on_push_and_back

Applying prettier in CI for pusher and back containers
This commit is contained in:
David Négrier 2021-06-24 10:29:21 +02:00 committed by GitHub
commit 52a90e52d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1872 additions and 1654 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,7 +1,7 @@
// lib/app.ts // lib/app.ts
import {PrometheusController} from "./Controller/PrometheusController"; import { PrometheusController } from "./Controller/PrometheusController";
import {DebugController} from "./Controller/DebugController"; import { DebugController } from "./Controller/DebugController";
import {App as uwsApp} from "./Server/sifrr.server"; import { App as uwsApp } from "./Server/sifrr.server";
class App { class App {
public app: uwsApp; public app: uwsApp;

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,53 +1,54 @@
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
import {stringify} from "circular-json"; import { stringify } from "circular-json";
import {HttpRequest, HttpResponse} from "uWebSockets.js"; import { HttpRequest, HttpResponse } from "uWebSockets.js";
import { parse } from 'query-string'; import { parse } from "query-string";
import {App} from "../Server/sifrr.server"; import { App } from "../Server/sifrr.server";
import {socketManager} from "../Services/SocketManager"; import { socketManager } from "../Services/SocketManager";
export class DebugController { export class DebugController {
constructor(private App : App) { constructor(private App: App) {
this.getDump(); this.getDump();
} }
getDump() {
getDump(){
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
const query = parse(req.getQuery()); const query = parse(req.getQuery());
if (query.token !== ADMIN_API_TOKEN) { if (query.token !== ADMIN_API_TOKEN) {
return res.status(401).send('Invalid token sent!'); return res.status(401).send("Invalid token sent!");
} }
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( return res
socketManager.getWorlds(), .writeStatus("200 OK")
(key: unknown, value: unknown) => { .writeHeader("Content-Type", "application/json")
if (key === 'listeners') { .end(
return 'Listeners'; stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
} if (key === "listeners") {
if (key === 'socket') { return "Listeners";
return 'Socket';
}
if (key === 'batchedMessages') {
return 'BatchedMessages';
}
if(value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
} }
return obj; if (key === "socket") {
} else if(value instanceof Set) { return "Socket";
}
if (key === "batchedMessages") {
return "BatchedMessages";
}
if (value instanceof Map) {
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
}
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = []; const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) { for (const [setKey, setValue] of value.entries()) {
obj.push(setValue); obj.push(setValue);
} }
return obj; return obj;
} else { } else {
return value; return value;
} }
} })
)); );
}); });
} }
} }

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();
@ -24,7 +21,7 @@ export class Admin {
this.socket.write(serverToAdminClientMessage); this.socket.write(serverToAdminClientMessage);
} }
public sendUserLeft(uuid: string/*, name: string, ip: string*/): void { public sendUserLeft(uuid: string /*, name: string, ip: string*/): void {
const serverToAdminClientMessage = new ServerToAdminClientMessage(); const serverToAdminClientMessage = new ServerToAdminClientMessage();
const userLeftRoomMessage = new UserLeftRoomMessage(); const userLeftRoomMessage = new UserLeftRoomMessage();

View File

@ -1,16 +1,16 @@
import {PointInterface} from "./Websocket/PointInterface"; import { PointInterface } from "./Websocket/PointInterface";
import {Group} from "./Group"; import { Group } from "./Group";
import {User, UserSocket} from "./User"; import { User, UserSocket } from "./User";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
import {PositionNotifier} from "./PositionNotifier"; import { PositionNotifier } from "./PositionNotifier";
import {Movable} from "_Model/Movable"; import { Movable } from "_Model/Movable";
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
import {arrayIntersect} from "../Services/ArrayHelper"; import { arrayIntersect } from "../Services/ArrayHelper";
import {EmoteEventMessage, JoinRoomMessage} from "../Messages/generated/messages_pb"; import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import {ZoneSocket} from "src/RoomManager"; import { ZoneSocket } from "src/RoomManager";
import {Admin} from "../Model/Admin"; import { Admin } from "../Model/Admin";
export type ConnectCallback = (user: User, group: Group) => void; export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void;
@ -39,33 +39,33 @@ export class GameRoom {
private readonly positionNotifier: PositionNotifier; private readonly positionNotifier: PositionNotifier;
public readonly roomId: string; public readonly roomId: string;
public readonly roomSlug: string; public readonly roomSlug: string;
public readonly worldSlug: string = ''; public readonly worldSlug: string = "";
public readonly organizationSlug: string = ''; public readonly organizationSlug: string = "";
private versionNumber:number = 1; private versionNumber: number = 1;
private nextUserId: number = 1; private nextUserId: number = 1;
constructor(roomId: string, constructor(
connectCallback: ConnectCallback, roomId: string,
disconnectCallback: DisconnectCallback, connectCallback: ConnectCallback,
minDistance: number, disconnectCallback: DisconnectCallback,
groupRadius: number, minDistance: number,
onEnters: EntersCallback, groupRadius: number,
onMoves: MovesCallback, onEnters: EntersCallback,
onLeaves: LeavesCallback, onMoves: MovesCallback,
onEmote: EmoteCallback, onLeaves: LeavesCallback,
onEmote: EmoteCallback
) { ) {
this.roomId = roomId; this.roomId = roomId;
if (isRoomAnonymous(roomId)) { if (isRoomAnonymous(roomId)) {
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
} else { } else {
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
this.roomSlug = roomSlug; this.roomSlug = roomSlug;
this.organizationSlug = organizationSlug; this.organizationSlug = organizationSlug;
this.worldSlug = worldSlug; this.worldSlug = worldSlug;
} }
this.users = new Map<number, User>(); this.users = new Map<number, User>();
this.usersByUuid = new Map<string, User>(); this.usersByUuid = new Map<string, User>();
this.admins = new Set<Admin>(); this.admins = new Set<Admin>();
@ -86,21 +86,22 @@ export class GameRoom {
return this.users; return this.users;
} }
public getUserByUuid(uuid: string): User|undefined { public getUserByUuid(uuid: string): User | undefined {
return this.usersByUuid.get(uuid); return this.usersByUuid.get(uuid);
} }
public getUserById(id: number): User|undefined { public getUserById(id: number): User | undefined {
return this.users.get(id); return this.users.get(id);
} }
public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User { public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User {
const positionMessage = joinRoomMessage.getPositionmessage(); const positionMessage = joinRoomMessage.getPositionmessage();
if (positionMessage === undefined) { if (positionMessage === undefined) {
throw new Error('Missing position message'); throw new Error("Missing position message");
} }
const position = ProtobufUtils.toPointInterface(positionMessage); const position = ProtobufUtils.toPointInterface(positionMessage);
const user = new User(this.nextUserId, const user = new User(
this.nextUserId,
joinRoomMessage.getUseruuid(), joinRoomMessage.getUseruuid(),
joinRoomMessage.getIpaddress(), joinRoomMessage.getIpaddress(),
position, position,
@ -126,12 +127,12 @@ export class GameRoom {
return user; return user;
} }
public leave(user : User){ public leave(user: User) {
const userObj = this.users.get(user.id); const userObj = this.users.get(user.id);
if (userObj === undefined) { if (userObj === undefined) {
console.warn('User ', user.id, 'does not belong to this game room! It should!'); console.warn("User ", user.id, "does not belong to this game room! It should!");
} }
if (userObj !== undefined && typeof userObj.group !== 'undefined') { if (userObj !== undefined && typeof userObj.group !== "undefined") {
this.leaveGroup(userObj); this.leaveGroup(userObj);
} }
this.users.delete(user.id); this.users.delete(user.id);
@ -143,7 +144,7 @@ export class GameRoom {
// Notify admins // Notify admins
for (const admin of this.admins) { for (const admin of this.admins) {
admin.sendUserLeft(user.uuid/*, user.name, user.IPAddress*/); admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/);
} }
} }
@ -151,7 +152,7 @@ export class GameRoom {
return this.users.size === 0 && this.admins.size === 0; return this.users.size === 0 && this.admins.size === 0;
} }
public updatePosition(user : User, userPosition: PointInterface): void { public updatePosition(user: User, userPosition: PointInterface): void {
user.setPosition(userPosition); user.setPosition(userPosition);
this.updateUserGroup(user); this.updateUserGroup(user);
@ -173,22 +174,24 @@ export class GameRoom {
return; return;
} }
const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user); const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user);
if (closestItem !== null) { if (closestItem !== null) {
if (closestItem instanceof Group) { if (closestItem instanceof Group) {
// Let's join the group! // Let's join the group!
closestItem.join(user); closestItem.join(user);
} else { } else {
const closestUser : User = closestItem; const closestUser: User = closestItem;
const group: Group = new Group(this.roomId,[ const group: Group = new Group(
user, this.roomId,
closestUser [user, closestUser],
], this.connectCallback, this.disconnectCallback, this.positionNotifier); this.connectCallback,
this.disconnectCallback,
this.positionNotifier
);
this.groups.add(group); this.groups.add(group);
} }
} }
} else { } else {
// If the user is part of a group: // If the user is part of a group:
// should he leave the group? // should he leave the group?
@ -229,7 +232,9 @@ export class GameRoom {
this.positionNotifier.leave(group); this.positionNotifier.leave(group);
group.destroy(); group.destroy();
if (!this.groups.has(group)) { if (!this.groups.has(group)) {
throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World."); throw new Error(
"Could not find group " + group.getId() + " referenced by user " + user.id + " in World."
);
} }
this.groups.delete(group); this.groups.delete(group);
//todo: is the group garbage collected? //todo: is the group garbage collected?
@ -247,16 +252,15 @@ export class GameRoom {
* OR * OR
* - close enough to a group (distance <= groupRadius) * - close enough to a group (distance <= groupRadius)
*/ */
private searchClosestAvailableUserOrGroup(user: User): User|Group|null private searchClosestAvailableUserOrGroup(user: User): User | Group | null {
{
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius); let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
let matchingItem: User | Group | null = null; let matchingItem: User | Group | null = null;
this.users.forEach((currentUser, userId) => { this.users.forEach((currentUser, userId) => {
// Let's only check users that are not part of a group // Let's only check users that are not part of a group
if (typeof currentUser.group !== 'undefined') { if (typeof currentUser.group !== "undefined") {
return; return;
} }
if(currentUser === user) { if (currentUser === user) {
return; return;
} }
if (currentUser.silent) { if (currentUser.silent) {
@ -265,7 +269,7 @@ export class GameRoom {
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
if(distance <= minimumDistanceFound && distance <= this.minDistance) { if (distance <= minimumDistanceFound && distance <= this.minDistance) {
minimumDistanceFound = distance; minimumDistanceFound = distance;
matchingItem = currentUser; matchingItem = currentUser;
} }
@ -276,7 +280,7 @@ export class GameRoom {
return; return;
} }
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
if(distance <= minimumDistanceFound && distance <= this.groupRadius) { if (distance <= minimumDistanceFound && distance <= this.groupRadius) {
minimumDistanceFound = distance; minimumDistanceFound = distance;
matchingItem = group; matchingItem = group;
} }
@ -285,15 +289,15 @@ export class GameRoom {
return matchingItem; return matchingItem;
} }
public static computeDistance(user1: User, user2: User): number public static computeDistance(user1: User, user2: User): number {
{
const user1Position = user1.getPosition(); const user1Position = user1.getPosition();
const user2Position = user2.getPosition(); const user2Position = user2.getPosition();
return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)); return Math.sqrt(
Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)
);
} }
public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number {
{
return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2));
} }
@ -325,9 +329,9 @@ export class GameRoom {
public adminLeave(admin: Admin): void { public adminLeave(admin: Admin): void {
this.admins.delete(admin); this.admins.delete(admin);
} }
public incrementVersion(): number { public incrementVersion(): number {
this.versionNumber++ this.versionNumber++;
return this.versionNumber; return this.versionNumber;
} }

View File

@ -1,13 +1,12 @@
import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { ConnectCallback, DisconnectCallback } from "./GameRoom";
import { User } from "./User"; import { User } from "./User";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import {Movable} from "_Model/Movable"; import { Movable } from "_Model/Movable";
import {PositionNotifier} from "_Model/PositionNotifier"; import { PositionNotifier } from "_Model/PositionNotifier";
import {gaugeManager} from "../Services/GaugeManager"; import { gaugeManager } from "../Services/GaugeManager";
import {MAX_PER_GROUP} from "../Enum/EnvironmentVariable"; import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
export class Group implements Movable { export class Group implements Movable {
private static nextId: number = 1; private static nextId: number = 1;
private id: number; private id: number;
@ -18,8 +17,13 @@ export class Group implements Movable {
private wasDestroyed: boolean = false; private wasDestroyed: boolean = false;
private roomId: string; private roomId: string;
constructor(
constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { roomId: string,
users: User[],
private connectCallback: ConnectCallback,
private disconnectCallback: DisconnectCallback,
private positionNotifier: PositionNotifier
) {
this.roomId = roomId; this.roomId = roomId;
this.users = new Set<User>(); this.users = new Set<User>();
this.id = Group.nextId; this.id = Group.nextId;
@ -43,7 +47,7 @@ export class Group implements Movable {
return Array.from(this.users.values()); return Array.from(this.users.values());
} }
getId() : number { getId(): number {
return this.id; return this.id;
} }
@ -53,7 +57,7 @@ export class Group implements Movable {
getPosition(): PositionInterface { getPosition(): PositionInterface {
return { return {
x: this.x, x: this.x,
y: this.y y: this.y,
}; };
} }
@ -83,7 +87,7 @@ export class Group implements Movable {
if (oldX === undefined) { if (oldX === undefined) {
this.positionNotifier.enter(this); this.positionNotifier.enter(this);
} else { } else {
this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY}); this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY });
} }
} }
@ -95,19 +99,17 @@ export class Group implements Movable {
return this.users.size <= 1; return this.users.size <= 1;
} }
join(user: User): void join(user: User): void {
{
// Broadcast on the right event // Broadcast on the right event
this.connectCallback(user, this); this.connectCallback(user, this);
this.users.add(user); this.users.add(user);
user.group = this; user.group = this;
} }
leave(user: User): void leave(user: User): void {
{
const success = this.users.delete(user); const success = this.users.delete(user);
if (success === false) { if (success === false) {
throw new Error("Could not find user "+user.id+" in the group "+this.id); throw new Error("Could not find user " + user.id + " in the group " + this.id);
} }
user.group = undefined; user.group = undefined;
@ -123,8 +125,7 @@ export class Group implements Movable {
* Let's kick everybody out. * Let's kick everybody out.
* Usually used when there is only one user left. * Usually used when there is only one user left.
*/ */
destroy(): void destroy(): void {
{
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId); if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
for (const user of this.users) { for (const user of this.users) {
this.leave(user); this.leave(user);
@ -132,7 +133,7 @@ export class Group implements Movable {
this.wasDestroyed = true; this.wasDestroyed = true;
} }
get getSize(){ get getSize() {
return this.users.size; return this.users.size;
} }
} }

View File

@ -1,8 +1,8 @@
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
/** /**
* A physical object that can be placed into a Zone * A physical object that can be placed into a Zone
*/ */
export interface Movable { export interface Movable {
getPosition(): PositionInterface getPosition(): PositionInterface;
} }

View File

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

View File

@ -8,12 +8,12 @@
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
* number of players around the current player. * number of players around the current player.
*/ */
import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone"; import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone";
import {Movable} from "_Model/Movable"; import { Movable } from "_Model/Movable";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import {ZoneSocket} from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import {User} from "_Model/User"; import { User } from "_Model/User";
import {EmoteEventMessage} from "../Messages/generated/messages_pb"; import { EmoteEventMessage } from "../Messages/generated/messages_pb";
interface ZoneDescriptor { interface ZoneDescriptor {
i: number; i: number;
@ -21,19 +21,24 @@ interface ZoneDescriptor {
} }
export class PositionNotifier { export class PositionNotifier {
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
private zones: Zone[][] = []; private zones: Zone[][] = [];
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback, private onEmote: EmoteCallback) { constructor(
} private zoneWidth: number,
private zoneHeight: number,
private onUserEnters: EntersCallback,
private onUserMoves: MovesCallback,
private onUserLeaves: LeavesCallback,
private onEmote: EmoteCallback
) {}
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
return { return {
i: Math.floor(x / this.zoneWidth), i: Math.floor(x / this.zoneWidth),
j: Math.floor(y / this.zoneHeight), j: Math.floor(y / this.zoneHeight),
} };
} }
public enter(thing: Movable): void { public enter(thing: Movable): void {
@ -100,6 +105,5 @@ export class PositionNotifier {
const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y);
const zone = this.getZone(zoneDesc.i, zoneDesc.j); const zone = this.getZone(zoneDesc.i, zoneDesc.j);
zone.emitEmoteEvent(emoteEventMessage); zone.emitEmoteEvent(emoteEventMessage);
} }
} }

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

@ -1,11 +1,17 @@
import { Group } from "./Group"; import { Group } from "./Group";
import { PointInterface } from "./Websocket/PointInterface"; import { PointInterface } from "./Websocket/PointInterface";
import {Zone} from "_Model/Zone"; import { Zone } from "_Model/Zone";
import {Movable} from "_Model/Movable"; import { Movable } from "_Model/Movable";
import {PositionNotifier} from "_Model/PositionNotifier"; import { PositionNotifier } from "_Model/PositionNotifier";
import {ServerDuplexStream} from "grpc"; import { ServerDuplexStream } from "grpc";
import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; import {
import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; BatchMessage,
CompanionMessage,
PusherToBackMessage,
ServerToClientMessage,
SubMessage,
} from "../Messages/generated/messages_pb";
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>; export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
@ -22,7 +28,7 @@ export class User implements Movable {
private positionNotifier: PositionNotifier, private positionNotifier: PositionNotifier,
public readonly socket: UserSocket, public readonly socket: UserSocket,
public readonly tags: string[], public readonly tags: string[],
public readonly visitCardUrl: string|null, public readonly visitCardUrl: string | null,
public readonly name: string, public readonly name: string,
public readonly characterLayers: CharacterLayer[], public readonly characterLayers: CharacterLayer[],
public readonly companion?: CompanionMessage public readonly companion?: CompanionMessage
@ -42,9 +48,8 @@ export class User implements Movable {
this.positionNotifier.updatePosition(this, position, oldPosition); this.positionNotifier.updatePosition(this, position, oldPosition);
} }
private batchedMessages: BatchMessage = new BatchMessage(); private batchedMessages: BatchMessage = new BatchMessage();
private batchTimeout: NodeJS.Timeout|null = null; private batchTimeout: NodeJS.Timeout | null = null;
public emitInBatch(payload: SubMessage): void { public emitInBatch(payload: SubMessage): void {
this.batchedMessages.addPayload(payload); this.batchedMessages.addPayload(payload);

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

@ -1,34 +1,33 @@
import {PointInterface} from "./PointInterface"; import { PointInterface } from "./PointInterface";
import { import {
CharacterLayerMessage, CharacterLayerMessage,
ItemEventMessage, ItemEventMessage,
PointMessage, PointMessage,
PositionMessage PositionMessage,
} from "../../Messages/generated/messages_pb"; } from "../../Messages/generated/messages_pb";
import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
export class ProtobufUtils { export class ProtobufUtils {
public static toPositionMessage(point: PointInterface): PositionMessage { public static toPositionMessage(point: PointInterface): PositionMessage {
let direction: Direction; let direction: Direction;
switch (point.direction) { switch (point.direction) {
case 'up': case "up":
direction = Direction.UP; direction = Direction.UP;
break; break;
case 'down': case "down":
direction = Direction.DOWN; direction = Direction.DOWN;
break; break;
case 'left': case "left":
direction = Direction.LEFT; direction = Direction.LEFT;
break; break;
case 'right': case "right":
direction = Direction.RIGHT; direction = Direction.RIGHT;
break; break;
default: default:
throw new Error('unexpected direction'); throw new Error("unexpected direction");
} }
const position = new PositionMessage(); const position = new PositionMessage();
@ -44,16 +43,16 @@ export class ProtobufUtils {
let direction: string; let direction: string;
switch (position.getDirection()) { switch (position.getDirection()) {
case Direction.UP: case Direction.UP:
direction = 'up'; direction = "up";
break; break;
case Direction.DOWN: case Direction.DOWN:
direction = 'down'; direction = "down";
break; break;
case Direction.LEFT: case Direction.LEFT:
direction = 'left'; direction = "left";
break; break;
case Direction.RIGHT: case Direction.RIGHT:
direction = 'right'; direction = "right";
break; break;
default: default:
throw new Error("Unexpected direction"); throw new Error("Unexpected direction");
@ -82,7 +81,7 @@ export class ProtobufUtils {
event: itemEventMessage.getEvent(), event: itemEventMessage.getEvent(),
parameters: JSON.parse(itemEventMessage.getParametersjson()), parameters: JSON.parse(itemEventMessage.getParametersjson()),
state: JSON.parse(itemEventMessage.getStatejson()), state: JSON.parse(itemEventMessage.getStatejson()),
} };
} }
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
@ -96,7 +95,7 @@ export class ProtobufUtils {
} }
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
return characterLayers.map(function(characterLayer): CharacterLayerMessage { return characterLayers.map(function (characterLayer): CharacterLayerMessage {
const message = new CharacterLayerMessage(); const message = new CharacterLayerMessage();
message.setName(characterLayer.name); message.setName(characterLayer.name);
if (characterLayer.url) { if (characterLayer.url) {
@ -107,7 +106,7 @@ export class ProtobufUtils {
} }
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] { public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
return characterLayers.map(function(characterLayer): CharacterLayer { return characterLayers.map(function (characterLayer): CharacterLayer {
const url = characterLayer.getUrl(); const url = characterLayer.getUrl();
return { return {
name: characterLayer.getName(), name: characterLayer.getName(),

View File

@ -1,35 +1,52 @@
import {User} from "./User"; import { User } from "./User";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
import {Movable} from "./Movable"; import { Movable } from "./Movable";
import {Group} from "./Group"; import { Group } from "./Group";
import {ZoneSocket} from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import {EmoteEventMessage} from "../Messages/generated/messages_pb"; import { EmoteEventMessage } from "../Messages/generated/messages_pb";
export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void; export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void; export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
export class Zone { export class Zone {
private things: Set<Movable> = new Set<Movable>(); private things: Set<Movable> = new Set<Movable>();
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>(); private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
constructor(
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { } private onEnters: EntersCallback,
private onMoves: MovesCallback,
private onLeaves: LeavesCallback,
private onEmote: EmoteCallback,
public readonly x: number,
public readonly y: number
) {}
/** /**
* A user/thing leaves the zone * A user/thing leaves the zone
*/ */
public leave(thing: Movable, newZone: Zone|null) { public leave(thing: Movable, newZone: Zone | null) {
const result = this.things.delete(thing); const result = this.things.delete(thing);
if (!result) { if (!result) {
if (thing instanceof User) { if (thing instanceof User) {
throw new Error('Could not find user in zone '+thing.id); throw new Error("Could not find user in zone " + thing.id);
} }
if (thing instanceof Group) { if (thing instanceof Group) {
throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')'); throw new Error(
"Could not find group " +
thing.getId() +
" in zone (" +
this.x +
"," +
this.y +
"). Position of group: (" +
thing.getPosition().x +
"," +
thing.getPosition().y +
")"
);
} }
} }
this.notifyLeft(thing, newZone); this.notifyLeft(thing, newZone);
} }
@ -37,13 +54,13 @@ export class Zone {
/** /**
* Notify listeners of this zone that this user/thing left * Notify listeners of this zone that this user/thing left
*/ */
private notifyLeft(thing: Movable, newZone: Zone|null) { private notifyLeft(thing: Movable, newZone: Zone | null) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.onLeaves(thing, newZone, listener); this.onLeaves(thing, newZone, listener);
} }
} }
public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
this.things.add(thing); this.things.add(thing);
this.notifyEnter(thing, oldZone, position); this.notifyEnter(thing, oldZone, position);
} }
@ -51,13 +68,12 @@ export class Zone {
/** /**
* Notify listeners of this zone that this user entered * Notify listeners of this zone that this user entered
*/ */
private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.onEnters(thing, oldZone, listener); this.onEnters(thing, oldZone, listener);
} }
} }
public move(thing: Movable, position: PositionInterface) { public move(thing: Movable, position: PositionInterface) {
if (!this.things.has(thing)) { if (!this.things.has(thing)) {
this.things.add(thing); this.things.add(thing);
@ -67,7 +83,7 @@ export class Zone {
for (const listener of this.listeners) { for (const listener of this.listeners) {
//if (listener !== thing) { //if (listener !== thing) {
this.onMoves(thing,position, listener); this.onMoves(thing, position, listener);
//} //}
} }
} }
@ -89,6 +105,5 @@ export class Zone {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.onEmote(emoteEventMessage, listener); this.onEmote(emoteEventMessage, listener);
} }
} }
} }

View File

@ -1,4 +1,4 @@
import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb"; import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb";
import { import {
AdminGlobalMessage, AdminGlobalMessage,
AdminMessage, AdminMessage,
@ -11,92 +11,114 @@ import {
JoinRoomMessage, JoinRoomMessage,
PlayGlobalMessage, PlayGlobalMessage,
PusherToBackMessage, PusherToBackMessage,
QueryJitsiJwtMessage, RefreshRoomPromptMessage, QueryJitsiJwtMessage,
RefreshRoomPromptMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
ServerToClientMessage, ServerToClientMessage,
SilentMessage, SilentMessage,
UserMovesMessage, UserMovesMessage,
WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, WebRtcSignalToServerMessage,
ZoneMessage WorldFullWarningToRoomMessage,
ZoneMessage,
} from "./Messages/generated/messages_pb"; } from "./Messages/generated/messages_pb";
import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc"; import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
import {socketManager} from "./Services/SocketManager"; import { socketManager } from "./Services/SocketManager";
import {emitError} from "./Services/MessageHelpers"; import { emitError } from "./Services/MessageHelpers";
import {User, UserSocket} from "./Model/User"; import { User, UserSocket } from "./Model/User";
import {GameRoom} from "./Model/GameRoom"; import { GameRoom } from "./Model/GameRoom";
import Debug from "debug"; import Debug from "debug";
import {Admin} from "./Model/Admin"; import { Admin } from "./Model/Admin";
const debug = Debug('roommanager'); const debug = Debug("roommanager");
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>; export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>; export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
const roomManager: IRoomManagerServer = { const roomManager: IRoomManagerServer = {
joinRoom: (call: UserSocket): void => { joinRoom: (call: UserSocket): void => {
console.log('joinRoom called'); console.log("joinRoom called");
let room: GameRoom|null = null; let room: GameRoom | null = null;
let user: User|null = null; let user: User | null = null;
call.on('data', (message: PusherToBackMessage) => { call.on("data", (message: PusherToBackMessage) => {
try { try {
if (room === null || user === null) { if (room === null || user === null) {
if (message.hasJoinroommessage()) { if (message.hasJoinroommessage()) {
socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => { socketManager
if (call.writable) { .handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
room = gameRoom; .then(({ room: gameRoom, user: myUser }) => {
user = myUser; if (call.writable) {
} else { room = gameRoom;
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user. user = myUser;
socketManager.leaveRoom(gameRoom, myUser); } else {
} //Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
}); socketManager.leaveRoom(gameRoom, myUser);
}
});
} else { } else {
throw new Error('The first message sent MUST be of type JoinRoomMessage'); throw new Error("The first message sent MUST be of type JoinRoomMessage");
} }
} else { } else {
if (message.hasJoinroommessage()) { if (message.hasJoinroommessage()) {
throw new Error('Cannot call JoinRoomMessage twice!'); throw new Error("Cannot call JoinRoomMessage twice!");
} else if (message.hasUsermovesmessage()) { } else if (message.hasUsermovesmessage()) {
socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage); socketManager.handleUserMovesMessage(
room,
user,
message.getUsermovesmessage() as UserMovesMessage
);
} else if (message.hasSilentmessage()) { } else if (message.hasSilentmessage()) {
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) { } else if (message.hasItemeventmessage()) {
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage); socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
} else if (message.hasWebrtcsignaltoservermessage()) { } else if (message.hasWebrtcsignaltoservermessage()) {
socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); socketManager.emitVideo(
room,
user,
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) { } else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); socketManager.emitScreenSharing(
room,
user,
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasPlayglobalmessage()) { } else if (message.hasPlayglobalmessage()) {
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasQueryjitsijwtmessage()){ } else if (message.hasQueryjitsijwtmessage()) {
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); socketManager.handleQueryJitsiJwtMessage(
} else if (message.hasEmotepromptmessage()){ user,
socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage); message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
}else if (message.hasSendusermessage()) { );
} else if (message.hasEmotepromptmessage()) {
socketManager.handleEmoteEventMessage(
room,
user,
message.getEmotepromptmessage() as EmotePromptMessage
);
} else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage(); const sendUserMessage = message.getSendusermessage();
if(sendUserMessage !== undefined) { if (sendUserMessage !== undefined) {
socketManager.handlerSendUserMessage(user, sendUserMessage); socketManager.handlerSendUserMessage(user, sendUserMessage);
} }
}else if (message.hasBanusermessage()) { } else if (message.hasBanusermessage()) {
const banUserMessage = message.getBanusermessage(); const banUserMessage = message.getBanusermessage();
if(banUserMessage !== undefined) { if (banUserMessage !== undefined) {
socketManager.handlerBanUserMessage(room, user, banUserMessage); socketManager.handlerBanUserMessage(room, user, banUserMessage);
} }
} else { } else {
throw new Error('Unhandled message type'); throw new Error("Unhandled message type");
} }
} }
} catch (e) { } catch (e) {
emitError(call, e); emitError(call, e);
call.end(); call.end();
} }
}); });
call.on('end', () => { call.on("end", () => {
debug('joinRoom ended'); debug("joinRoom ended");
if (user !== null && room !== null) { if (user !== null && room !== null) {
socketManager.leaveRoom(room, user); socketManager.leaveRoom(room, user);
} }
@ -105,41 +127,40 @@ const roomManager: IRoomManagerServer = {
user = null; user = null;
}); });
call.on('error', (err: Error) => { call.on("error", (err: Error) => {
console.error('An error occurred in joinRoom stream:', err); console.error("An error occurred in joinRoom stream:", err);
}); });
}, },
listenZone(call: ZoneSocket): void { listenZone(call: ZoneSocket): void {
debug('listenZone called'); debug("listenZone called");
const zoneMessage = call.request; const zoneMessage = call.request;
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
call.on('cancelled', () => { call.on("cancelled", () => {
debug('listenZone cancelled'); debug("listenZone cancelled");
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
call.end(); call.end();
}) });
call.on('close', () => { call.on("close", () => {
debug('listenZone connection closed'); debug("listenZone connection closed");
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
}).on('error', (e) => { }).on("error", (e) => {
console.error('An error occurred in listenZone stream:', e); console.error("An error occurred in listenZone stream:", e);
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
call.end(); call.end();
}); });
}, },
adminRoom(call: AdminSocket): void { adminRoom(call: AdminSocket): void {
console.log('adminRoom called'); console.log("adminRoom called");
const admin = new Admin(call); const admin = new Admin(call);
let room: GameRoom|null = null; let room: GameRoom | null = null;
call.on('data', (message: AdminPusherToBackMessage) => { call.on("data", (message: AdminPusherToBackMessage) => {
try { try {
if (room === null) { if (room === null) {
if (message.hasSubscribetoroom()) { if (message.hasSubscribetoroom()) {
@ -148,18 +169,17 @@ const roomManager: IRoomManagerServer = {
room = gameRoom; room = gameRoom;
}); });
} else { } else {
throw new Error('The first message sent MUST be of type JoinRoomMessage'); throw new Error("The first message sent MUST be of type JoinRoomMessage");
} }
} }
} catch (e) { } catch (e) {
emitError(call, e); emitError(call, e);
call.end(); call.end();
} }
}); });
call.on('end', () => { call.on("end", () => {
debug('joinRoom ended'); debug("joinRoom ended");
if (room !== null) { if (room !== null) {
socketManager.leaveAdminRoom(room, admin); socketManager.leaveAdminRoom(room, admin);
} }
@ -167,18 +187,21 @@ const roomManager: IRoomManagerServer = {
room = null; room = null;
}); });
call.on('error', (err: Error) => { call.on("error", (err: Error) => {
console.error('An error occurred in joinAdminRoom stream:', err); console.error("An error occurred in joinAdminRoom stream:", err);
}); });
}, },
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void { sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
socketManager.sendAdminMessage(
socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage()); call.request.getRoomid(),
call.request.getRecipientuuid(),
call.request.getMessage()
);
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void { sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
throw new Error('Not implemented yet'); throw new Error("Not implemented yet");
// TODO // TODO
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
@ -192,14 +215,20 @@ const roomManager: IRoomManagerServer = {
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()); socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
sendWorldFullWarningToRoom(call: ServerUnaryCall<WorldFullWarningToRoomMessage>, callback: sendUnaryData<EmptyMessage>): void { sendWorldFullWarningToRoom(
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
callback: sendUnaryData<EmptyMessage>
): void {
socketManager.dispatchWorlFullWarning(call.request.getRoomid()); socketManager.dispatchWorlFullWarning(call.request.getRoomid());
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
sendRefreshRoomPrompt(call: ServerUnaryCall<RefreshRoomPromptMessage>, callback: sendUnaryData<EmptyMessage>): void { sendRefreshRoomPrompt(
call: ServerUnaryCall<RefreshRoomPromptMessage>,
callback: sendUnaryData<EmptyMessage>
): void {
socketManager.dispatchRoomRefresh(call.request.getRoomid()); socketManager.dispatchRoomRefresh(call.request.getRoomid());
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },
}; };
export {roomManager}; export { roomManager };

View File

@ -1,13 +1,13 @@
import { App as _App, AppOptions } from 'uWebSockets.js'; import { App as _App, AppOptions } from "uWebSockets.js";
import BaseApp from './baseapp'; import BaseApp from "./baseapp";
import { extend } from './utils'; import { extend } from "./utils";
import { UwsApp } from './types'; import { UwsApp } from "./types";
class App extends (<UwsApp>_App) { class App extends (<UwsApp>_App) {
constructor(options: AppOptions = {}) { constructor(options: AppOptions = {}) {
super(options); // eslint-disable-line constructor-super super(options); // eslint-disable-line constructor-super
extend(this, new BaseApp()); extend(this, new BaseApp());
} }
} }
export default App; export default App;

View File

@ -1,116 +1,109 @@
import { Readable } from 'stream'; import { Readable } from "stream";
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
import formData from './formdata'; import formData from "./formdata";
import { stob } from './utils'; import { stob } from "./utils";
import { Handler } from './types'; import { Handler } from "./types";
import {join} from "path"; import { join } from "path";
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
const noOp = () => true; const noOp = () => true;
const handleBody = (res: HttpResponse, req: HttpRequest) => { const handleBody = (res: HttpResponse, req: HttpRequest) => {
const contType = req.getHeader('content-type'); const contType = req.getHeader("content-type");
res.bodyStream = function() { res.bodyStream = function () {
const stream = new Readable(); const stream = new Readable();
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
this.onData((ab: ArrayBuffer, isLast: boolean) => { this.onData((ab: ArrayBuffer, isLast: boolean) => {
// uint and then slicing is bit faster than slice and then uint // uint and then slicing is bit faster than slice and then uint
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
if (isLast) { if (isLast) {
stream.push(null); stream.push(null);
} }
}); });
return stream; return stream;
}; };
res.body = () => stob(res.bodyStream()); res.body = () => stob(res.bodyStream());
if (contType.includes('application/json')) if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
res.json = async () => JSON.parse(await res.body()); if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
if (contTypes.map(t => contType.includes(t)).includes(true))
res.formData = formData.bind(res, contType);
}; };
class BaseApp { class BaseApp {
_sockets = new Map(); _sockets = new Map();
ws!: TemplatedApp['ws']; ws!: TemplatedApp["ws"];
get!: TemplatedApp['get']; get!: TemplatedApp["get"];
_post!: TemplatedApp['post']; _post!: TemplatedApp["post"];
_put!: TemplatedApp['put']; _put!: TemplatedApp["put"];
_patch!: TemplatedApp['patch']; _patch!: TemplatedApp["patch"];
_listen!: TemplatedApp['listen']; _listen!: TemplatedApp["listen"];
post(pattern: string, handler: Handler) { post(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._post(pattern, (res, req) => {
this._post(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req); handler(res, req);
handler(res, req); });
}); return this;
return this; }
}
put(pattern: string, handler: Handler) { put(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._put(pattern, (res, req) => {
this._put(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req);
handler(res, req); handler(res, req);
}); });
return this; return this;
} }
patch(pattern: string, handler: Handler) { patch(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._patch(pattern, (res, req) => {
this._patch(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req);
handler(res, req); handler(res, req);
}); });
return this; return this;
} }
listen(h: string | number, p: Function | number = noOp, cb?: Function) { listen(h: string | number, p: Function | number = noOp, cb?: Function) {
if (typeof p === 'number' && typeof h === 'string') { if (typeof p === "number" && typeof h === "string") {
this._listen(h, p, socket => { this._listen(h, p, (socket) => {
this._sockets.set(p, socket); this._sockets.set(p, socket);
if (cb === undefined) { if (cb === undefined) {
throw new Error('cb undefined'); throw new Error("cb undefined");
}
cb(socket);
});
} else if (typeof h === "number" && typeof p === "function") {
this._listen(h, (socket) => {
this._sockets.set(h, socket);
p(socket);
});
} else {
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
} }
cb(socket);
}); return this;
} else if (typeof h === 'number' && typeof p === 'function') {
this._listen(h, socket => {
this._sockets.set(h, socket);
p(socket);
});
} else {
throw Error(
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
);
} }
return this; close(port: null | number = null) {
} if (port) {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
close(port: null | number = null) { this._sockets.delete(port);
if (port) { } else {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); this._sockets.forEach((app) => {
this._sockets.delete(port); us_listen_socket_close(app);
} else { });
this._sockets.forEach(app => { this._sockets.clear();
us_listen_socket_close(app); }
}); return this;
this._sockets.clear();
} }
return this;
}
} }
export default BaseApp; export default BaseApp;

View File

@ -1,100 +1,99 @@
import { createWriteStream } from 'fs'; import { createWriteStream } from "fs";
import { join, dirname } from 'path'; import { join, dirname } from "path";
import Busboy from 'busboy'; import Busboy from "busboy";
import mkdirp from 'mkdirp'; import mkdirp from "mkdirp";
function formData( function formData(
contType: string, contType: string,
options: busboy.BusboyConfig & { options: busboy.BusboyConfig & {
abortOnLimit?: boolean; abortOnLimit?: boolean;
tmpDir?: string; tmpDir?: string;
onFile?: ( onFile?: (
fieldname: string, fieldname: string,
file: NodeJS.ReadableStream, file: NodeJS.ReadableStream,
filename: string, filename: string,
encoding: string, encoding: string,
mimetype: string mimetype: string
) => string; ) => string;
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
filename?: (oldName: string) => string; filename?: (oldName: string) => string;
} = {} } = {}
) { ) {
console.log('Enter form data'); console.log("Enter form data");
options.headers = { options.headers = {
'content-type': contType "content-type": contType,
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const busb = new Busboy(options); const busb = new Busboy(options);
const ret = {}; const ret = {};
this.bodyStream().pipe(busb); this.bodyStream().pipe(busb);
busb.on('limit', () => { busb.on("limit", () => {
if (options.abortOnLimit) { if (options.abortOnLimit) {
reject(Error('limit')); reject(Error("limit"));
} }
});
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
filename,
encoding,
mimetype,
filePath: undefined,
};
if (typeof options.tmpDir === "string") {
if (typeof options.filename === "function") filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave;
}
if (typeof options.onFile === "function") {
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
}
setRetValue(ret, fieldname, value);
});
busb.on("field", function (fieldname, value) {
if (typeof options.onField === "function") options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on("finish", function () {
resolve(ret);
});
busb.on("error", reject);
}); });
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = {
filename,
encoding,
mimetype,
filePath: undefined
};
if (typeof options.tmpDir === 'string') {
if (typeof options.filename === 'function') filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave;
}
if (typeof options.onFile === 'function') {
value.filePath =
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
}
setRetValue(ret, fieldname, value);
});
busb.on('field', function(fieldname, value) {
if (typeof options.onField === 'function') options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on('finish', function() {
resolve(ret);
});
busb.on('error', reject);
});
} }
function setRetValue( function setRetValue(
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
fieldname: string, fieldname: string,
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
) { ) {
if (fieldname.endsWith('[]')) { if (fieldname.endsWith("[]")) {
fieldname = fieldname.slice(0, fieldname.length - 2); fieldname = fieldname.slice(0, fieldname.length - 2);
if (Array.isArray(ret[fieldname])) { if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value); ret[fieldname].push(value);
} else {
ret[fieldname] = [value];
}
} else { } else {
ret[fieldname] = [value]; if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else if (ret[fieldname]) {
ret[fieldname] = [ret[fieldname], value];
} else {
ret[fieldname] = value;
}
} }
} else {
if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else if (ret[fieldname]) {
ret[fieldname] = [ret[fieldname], value];
} else {
ret[fieldname] = value;
}
}
} }
export default formData; export default formData;

View File

@ -1,13 +1,13 @@
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
import BaseApp from './baseapp'; import BaseApp from "./baseapp";
import { extend } from './utils'; import { extend } from "./utils";
import { UwsApp } from './types'; import { UwsApp } from "./types";
class SSLApp extends (<UwsApp>_SSLApp) { class SSLApp extends (<UwsApp>_SSLApp) {
constructor(options: AppOptions) { constructor(options: AppOptions) {
super(options); // eslint-disable-line constructor-super super(options); // eslint-disable-line constructor-super
extend(this, new BaseApp()); extend(this, new BaseApp());
} }
} }
export default SSLApp; export default SSLApp;

View File

@ -1,9 +1,9 @@
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
export type UwsApp = { export type UwsApp = {
(options: AppOptions): TemplatedApp; (options: AppOptions): TemplatedApp;
new (options: AppOptions): TemplatedApp; new (options: AppOptions): TemplatedApp;
prototype: TemplatedApp; prototype: TemplatedApp;
}; };
export type Handler = (res: HttpResponse, req: HttpRequest) => void; export type Handler = (res: HttpResponse, req: HttpRequest) => void;

View File

@ -1,37 +1,36 @@
import { ReadStream } from 'fs'; import { ReadStream } from "fs";
function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( function extend(who: any, from: any, overwrite = true) {
Object.keys(from) const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
); ownProps.forEach((prop) => {
ownProps.forEach(prop => { if (prop === "constructor" || from[prop] === undefined) return;
if (prop === 'constructor' || from[prop] === undefined) return; if (who[prop] && overwrite) {
if (who[prop] && overwrite) { who[`_${prop}`] = who[prop];
who[`_${prop}`] = who[prop]; }
} if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); else who[prop] = from[prop];
else who[prop] = from[prop]; });
});
} }
function stob(stream: ReadStream): Promise<Buffer> { function stob(stream: ReadStream): Promise<Buffer> {
return new Promise(resolve => { return new Promise((resolve) => {
const buffers: Buffer[] = []; const buffers: Buffer[] = [];
stream.on('data', buffers.push.bind(buffers)); stream.on("data", buffers.push.bind(buffers));
stream.on('end', () => { stream.on("end", () => {
switch (buffers.length) { switch (buffers.length) {
case 0: case 0:
resolve(Buffer.allocUnsafe(0)); resolve(Buffer.allocUnsafe(0));
break; break;
case 1: case 1:
resolve(buffers[0]); resolve(buffers[0]);
break; break;
default: default:
resolve(Buffer.concat(buffers)); resolve(Buffer.concat(buffers));
} }
});
}); });
});
} }
export { extend, stob }; export { extend, stob };

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

@ -1,6 +1,6 @@
import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
function secNSec2ms(secNSec: Array<number>|number) { function secNSec2ms(secNSec: Array<number> | number) {
if (Array.isArray(secNSec)) { if (Array.isArray(secNSec)) {
return secNSec[0] * 1000 + secNSec[1] / 1000000; return secNSec[0] * 1000 + secNSec[1] / 1000000;
} }
@ -12,17 +12,17 @@ class CpuTracker {
private overHeating: boolean = false; private overHeating: boolean = false;
constructor() { constructor() {
let time = process.hrtime.bigint() let time = process.hrtime.bigint();
let usage = process.cpuUsage() let usage = process.cpuUsage();
setInterval(() => { setInterval(() => {
const elapTime = process.hrtime.bigint(); const elapTime = process.hrtime.bigint();
const elapUsage = process.cpuUsage(usage) const elapUsage = process.cpuUsage(usage);
usage = process.cpuUsage() usage = process.cpuUsage();
const elapTimeMS = elapTime - time; const elapTimeMS = elapTime - time;
const elapUserMS = secNSec2ms(elapUsage.user) const elapUserMS = secNSec2ms(elapUsage.user);
const elapSystMS = secNSec2ms(elapUsage.system) const elapSystMS = secNSec2ms(elapUsage.system);
this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
time = elapTime; time = elapTime;

View File

@ -1,4 +1,4 @@
import {Counter, Gauge} from "prom-client"; import { Counter, Gauge } from "prom-client";
//this class should manage all the custom metrics used by prometheus //this class should manage all the custom metrics used by prometheus
class GaugeManager { class GaugeManager {
@ -10,29 +10,29 @@ class GaugeManager {
constructor() { constructor() {
this.nbRoomsGauge = new Gauge({ this.nbRoomsGauge = new Gauge({
name: 'workadventure_nb_rooms', name: "workadventure_nb_rooms",
help: 'Number of active rooms' help: "Number of active rooms",
}); });
this.nbClientsGauge = new Gauge({ this.nbClientsGauge = new Gauge({
name: 'workadventure_nb_sockets', name: "workadventure_nb_sockets",
help: 'Number of connected sockets', help: "Number of connected sockets",
labelNames: [ ] labelNames: [],
}); });
this.nbClientsPerRoomGauge = new Gauge({ this.nbClientsPerRoomGauge = new Gauge({
name: 'workadventure_nb_clients_per_room', name: "workadventure_nb_clients_per_room",
help: 'Number of clients per room', help: "Number of clients per room",
labelNames: [ 'room' ] labelNames: ["room"],
}); });
this.nbGroupsPerRoomCounter = new Counter({ this.nbGroupsPerRoomCounter = new Counter({
name: 'workadventure_counter_groups_per_room', name: "workadventure_counter_groups_per_room",
help: 'Counter of groups per room', help: "Counter of groups per room",
labelNames: [ 'room' ] labelNames: ["room"],
}); });
this.nbGroupsPerRoomGauge = new Gauge({ this.nbGroupsPerRoomGauge = new Gauge({
name: 'workadventure_nb_groups_per_room', name: "workadventure_nb_groups_per_room",
help: 'Number of groups per room', help: "Number of groups per room",
labelNames: [ 'room' ] labelNames: ["room"],
}); });
} }
@ -54,13 +54,13 @@ class GaugeManager {
} }
incNbGroupsPerRoomGauge(roomId: string): void { incNbGroupsPerRoomGauge(roomId: string): void {
this.nbGroupsPerRoomCounter.inc({ room: roomId }) this.nbGroupsPerRoomCounter.inc({ room: roomId });
this.nbGroupsPerRoomGauge.inc({ room: roomId }) this.nbGroupsPerRoomGauge.inc({ room: roomId });
} }
decNbGroupsPerRoomGauge(roomId: string): void { decNbGroupsPerRoomGauge(roomId: string): void {
this.nbGroupsPerRoomGauge.dec({ room: roomId }) this.nbGroupsPerRoomGauge.dec({ room: roomId });
} }
} }
export const gaugeManager = new GaugeManager(); export const gaugeManager = new GaugeManager();

View File

@ -1,5 +1,5 @@
import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb"; import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb";
import {UserSocket} from "_Model/User"; import { UserSocket } from "_Model/User";
export function emitError(Client: UserSocket, message: string): void { export function emitError(Client: UserSocket, message: string): void {
const errorMessage = new ErrorMessage(); const errorMessage = new ErrorMessage();
@ -9,7 +9,7 @@ export function emitError(Client: UserSocket, message: string): void {
serverToClientMessage.setErrormessage(errorMessage); serverToClientMessage.setErrormessage(errorMessage);
//if (!Client.disconnecting) { //if (!Client.disconnecting) {
Client.write(serverToClientMessage); Client.write(serverToClientMessage);
//} //}
console.warn(message); console.warn(message);
} }

View File

@ -1,4 +1,4 @@
import {GameRoom} from "../Model/GameRoom"; import { GameRoom } from "../Model/GameRoom";
import { import {
ItemEventMessage, ItemEventMessage,
ItemStateMessage, ItemStateMessage,
@ -27,39 +27,39 @@ import {
WorldFullWarningMessage, WorldFullWarningMessage,
UserLeftZoneMessage, UserLeftZoneMessage,
EmoteEventMessage, EmoteEventMessage,
BanUserMessage, RefreshRoomMessage, EmotePromptMessage, BanUserMessage,
RefreshRoomMessage,
EmotePromptMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {User, UserSocket} from "../Model/User"; import { User, UserSocket } from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import {Group} from "../Model/Group"; import { Group } from "../Model/Group";
import {cpuTracker} from "./CpuTracker"; import { cpuTracker } from "./CpuTracker";
import { import {
GROUP_RADIUS, GROUP_RADIUS,
JITSI_ISS, JITSI_ISS,
MINIMUM_DISTANCE, MINIMUM_DISTANCE,
SECRET_JITSI_KEY, SECRET_JITSI_KEY,
TURN_STATIC_AUTH_SECRET TURN_STATIC_AUTH_SECRET,
} from "../Enum/EnvironmentVariable"; } from "../Enum/EnvironmentVariable";
import {Movable} from "../Model/Movable"; import { Movable } from "../Model/Movable";
import {PositionInterface} from "../Model/PositionInterface"; import { PositionInterface } from "../Model/PositionInterface";
import Jwt from "jsonwebtoken"; import Jwt from "jsonwebtoken";
import {JITSI_URL} from "../Enum/EnvironmentVariable"; import { JITSI_URL } from "../Enum/EnvironmentVariable";
import {clientEventsEmitter} from "./ClientEventsEmitter"; import { clientEventsEmitter } from "./ClientEventsEmitter";
import {gaugeManager} from "./GaugeManager"; import { gaugeManager } from "./GaugeManager";
import {ZoneSocket} from "../RoomManager"; import { ZoneSocket } from "../RoomManager";
import {Zone} from "_Model/Zone"; import { Zone } from "_Model/Zone";
import Debug from "debug"; import Debug from "debug";
import {Admin} from "_Model/Admin"; import { Admin } from "_Model/Admin";
import crypto from "crypto"; import crypto from "crypto";
const debug = Debug("sockermanager");
const debug = Debug('sockermanager');
function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void { function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void {
// TODO: should we batch those every 100ms? // TODO: should we batch those every 100ms?
const batchMessage = new BatchToPusherMessage(); const batchMessage = new BatchToPusherMessage();
batchMessage.addPayload(subMessage); batchMessage.addPayload(subMessage);
socket.write(batchMessage); socket.write(batchMessage);
} }
@ -68,7 +68,6 @@ export class SocketManager {
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>(); private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
constructor() { constructor() {
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
gaugeManager.incNbClientPerRoomGauge(roomId); gaugeManager.incNbClientPerRoomGauge(roomId);
}); });
@ -77,16 +76,18 @@ export class SocketManager {
}); });
} }
public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { public async handleJoinRoom(
socket: UserSocket,
joinRoomMessage: JoinRoomMessage
): Promise<{ room: GameRoom; user: User }> {
//join new previous room //join new previous room
const {room, user} = await this.joinRoom(socket, joinRoomMessage); const { room, user } = await this.joinRoom(socket, joinRoomMessage);
if (!socket.writable) { if (!socket.writable) {
console.warn('Socket was aborted'); console.warn("Socket was aborted");
return { return {
room, room,
user user,
}; };
} }
const roomJoinedMessage = new RoomJoinedMessage(); const roomJoinedMessage = new RoomJoinedMessage();
@ -108,9 +109,8 @@ export class SocketManager {
return { return {
room, room,
user user,
}; };
} }
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) { handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
@ -124,13 +124,12 @@ export class SocketManager {
} }
if (position === undefined) { if (position === undefined) {
throw new Error('Position not found in message'); throw new Error("Position not found in message");
} }
const viewport = userMoves.viewport; const viewport = userMoves.viewport;
if (viewport === undefined) { if (viewport === undefined) {
throw new Error('Viewport not found in message'); throw new Error("Viewport not found in message");
} }
// update position in the world // update position in the world
room.updatePosition(user, ProtobufUtils.toPointInterface(position)); room.updatePosition(user, ProtobufUtils.toPointInterface(position));
@ -189,7 +188,11 @@ export class SocketManager {
//send only at user //send only at user
const remoteUser = room.getUsers().get(data.getReceiverid()); const remoteUser = room.getUsers().get(data.getReceiverid());
if (remoteUser === undefined) { if (remoteUser === undefined) {
console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); console.warn(
"While exchanging a WebRTC signal: client with id ",
data.getReceiverid(),
" does not exist. This might be a race condition."
);
return; return;
} }
@ -197,8 +200,8 @@ export class SocketManager {
webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal()); webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer" // TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== '') { if (TURN_STATIC_AUTH_SECRET !== "") {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password); webrtcSignalToClient.setWebrtcpassword(password);
} }
@ -207,7 +210,7 @@ export class SocketManager {
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
//if (!client.disconnecting) { //if (!client.disconnecting) {
remoteUser.socket.write(serverToClientMessage); remoteUser.socket.write(serverToClientMessage);
//} //}
} }
@ -215,7 +218,11 @@ export class SocketManager {
//send only at user //send only at user
const remoteUser = room.getUsers().get(data.getReceiverid()); const remoteUser = room.getUsers().get(data.getReceiverid());
if (remoteUser === undefined) { if (remoteUser === undefined) {
console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); console.warn(
"While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ",
data.getReceiverid(),
" does not exist. This might be a race condition."
);
return; return;
} }
@ -223,8 +230,8 @@ export class SocketManager {
webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal()); webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer" // TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== '') { if (TURN_STATIC_AUTH_SECRET !== "") {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password); webrtcSignalToClient.setWebrtcpassword(password);
} }
@ -233,11 +240,11 @@ export class SocketManager {
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
//if (!client.disconnecting) { //if (!client.disconnecting) {
remoteUser.socket.write(serverToClientMessage); remoteUser.socket.write(serverToClientMessage);
//} //}
} }
leaveRoom(room: GameRoom, user: User){ leaveRoom(room: GameRoom, user: User) {
// leave previous room and world // leave previous room and world
try { try {
//user leave previous world //user leave previous world
@ -249,33 +256,39 @@ export class SocketManager {
} }
} finally { } finally {
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
console.log('A user left'); console.log("A user left");
} }
} }
async getOrCreateRoom(roomId: string): Promise<GameRoom> { async getOrCreateRoom(roomId: string): Promise<GameRoom> {
//check and create new world for a room //check and create new world for a room
let world = this.rooms.get(roomId) let world = this.rooms.get(roomId);
if(world === undefined){ if (world === undefined) {
world = new GameRoom( world = new GameRoom(
roomId, roomId,
(user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.joinWebRtcRoom(user, group),
(user: User, group: Group) => this.disConnectedUser(user, group), (user: User, group: Group) => this.disConnectedUser(user, group),
MINIMUM_DISTANCE, MINIMUM_DISTANCE,
GROUP_RADIUS, GROUP_RADIUS,
(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener), (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
(thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), this.onZoneEnter(thing, fromZone, listener),
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener), (thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
(emoteEventMessage:EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener), this.onClientMove(thing, position, listener),
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
this.onClientLeave(thing, newZone, listener),
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
this.onEmote(emoteEventMessage, listener)
); );
gaugeManager.incNbRoomGauge(); gaugeManager.incNbRoomGauge();
this.rooms.set(roomId, world); this.rooms.set(roomId, world);
} }
return Promise.resolve(world) return Promise.resolve(world);
} }
private async joinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { private async joinRoom(
socket: UserSocket,
joinRoomMessage: JoinRoomMessage
): Promise<{ room: GameRoom; user: User }> {
const roomId = joinRoomMessage.getRoomid(); const roomId = joinRoomMessage.getRoomid();
const room = await socketManager.getOrCreateRoom(roomId); const room = await socketManager.getOrCreateRoom(roomId);
@ -284,15 +297,15 @@ export class SocketManager {
const user = room.join(socket, joinRoomMessage); const user = room.join(socket, joinRoomMessage);
clientEventsEmitter.emitClientJoin(user.uuid, roomId); clientEventsEmitter.emitClientJoin(user.uuid, roomId);
console.log(new Date().toISOString() + ' A user joined'); console.log(new Date().toISOString() + " A user joined");
return {room, user}; return { room, user };
} }
private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) { private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) {
if (thing instanceof User) { if (thing instanceof User) {
const userJoinedZoneMessage = new UserJoinedZoneMessage(); const userJoinedZoneMessage = new UserJoinedZoneMessage();
if (!Number.isInteger(thing.id)) { if (!Number.isInteger(thing.id)) {
throw new Error('clientUser.userId is not an integer '+thing.id); throw new Error("clientUser.userId is not an integer " + thing.id);
} }
userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setName(thing.name);
@ -312,11 +325,11 @@ export class SocketManager {
} else if (thing instanceof Group) { } else if (thing instanceof Group) {
this.emitCreateUpdateGroupEvent(listener, fromZone, thing); this.emitCreateUpdateGroupEvent(listener, fromZone, thing);
} else { } else {
console.error('Unexpected type for Movable.'); console.error("Unexpected type for Movable.");
} }
} }
private onClientMove(thing: Movable, position:PositionInterface, listener: ZoneSocket): void { private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void {
if (thing instanceof User) { if (thing instanceof User) {
const userMovedMessage = new UserMovedMessage(); const userMovedMessage = new UserMovedMessage();
userMovedMessage.setUserid(thing.id); userMovedMessage.setUserid(thing.id);
@ -331,21 +344,20 @@ export class SocketManager {
} else if (thing instanceof Group) { } else if (thing instanceof Group) {
this.emitCreateUpdateGroupEvent(listener, null, thing); this.emitCreateUpdateGroupEvent(listener, null, thing);
} else { } else {
console.error('Unexpected type for Movable.'); console.error("Unexpected type for Movable.");
} }
} }
private onClientLeave(thing: Movable, newZone: Zone|null, listener: ZoneSocket) { private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) {
if (thing instanceof User) { if (thing instanceof User) {
this.emitUserLeftEvent(listener, thing.id, newZone); this.emitUserLeftEvent(listener, thing.id, newZone);
} else if (thing instanceof Group) { } else if (thing instanceof Group) {
this.emitDeleteGroupEvent(listener, thing.getId(), newZone); this.emitDeleteGroupEvent(listener, thing.getId(), newZone);
} else { } else {
console.error('Unexpected type for Movable.'); console.error("Unexpected type for Movable.");
} }
} }
private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) { private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) {
const subMessage = new SubToPusherMessage(); const subMessage = new SubToPusherMessage();
subMessage.setEmoteeventmessage(emoteEventMessage); subMessage.setEmoteeventmessage(emoteEventMessage);
@ -353,7 +365,7 @@ export class SocketManager {
emitZoneMessage(subMessage, client); emitZoneMessage(subMessage, client);
} }
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void { private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
const position = group.getPosition(); const position = group.getPosition();
const pointMessage = new PointMessage(); const pointMessage = new PointMessage();
pointMessage.setX(Math.floor(position.x)); pointMessage.setX(Math.floor(position.x));
@ -371,7 +383,7 @@ export class SocketManager {
//client.emitInBatch(subMessage); //client.emitInBatch(subMessage);
} }
private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone|null): void { private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void {
const groupDeleteMessage = new GroupLeftZoneMessage(); const groupDeleteMessage = new GroupLeftZoneMessage();
groupDeleteMessage.setGroupid(groupId); groupDeleteMessage.setGroupid(groupId);
groupDeleteMessage.setTozone(this.toProtoZone(newZone)); groupDeleteMessage.setTozone(this.toProtoZone(newZone));
@ -383,7 +395,7 @@ export class SocketManager {
//user.emitInBatch(subMessage); //user.emitInBatch(subMessage);
} }
private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone|null): void { private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void {
const userLeftMessage = new UserLeftZoneMessage(); const userLeftMessage = new UserLeftZoneMessage();
userLeftMessage.setUserid(userId); userLeftMessage.setUserid(userId);
userLeftMessage.setTozone(this.toProtoZone(newZone)); userLeftMessage.setTozone(this.toProtoZone(newZone));
@ -394,7 +406,7 @@ export class SocketManager {
emitZoneMessage(subMessage, client); emitZoneMessage(subMessage, client);
} }
private toProtoZone(zone: Zone|null): ProtoZone|undefined { private toProtoZone(zone: Zone | null): ProtoZone | undefined {
if (zone !== null) { if (zone !== null) {
const zoneMessage = new ProtoZone(); const zoneMessage = new ProtoZone();
zoneMessage.setX(zone.x); zoneMessage.setX(zone.x);
@ -405,7 +417,6 @@ export class SocketManager {
} }
private joinWebRtcRoom(user: User, group: Group) { private joinWebRtcRoom(user: User, group: Group) {
for (const otherUser of group.getUsers()) { for (const otherUser of group.getUsers()) {
if (user === otherUser) { if (user === otherUser) {
continue; continue;
@ -416,8 +427,8 @@ export class SocketManager {
webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setUserid(otherUser.id);
webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setName(otherUser.name);
webrtcStartMessage1.setInitiator(true); webrtcStartMessage1.setInitiator(true);
if (TURN_STATIC_AUTH_SECRET !== '') { if (TURN_STATIC_AUTH_SECRET !== "") {
const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
webrtcStartMessage1.setWebrtcusername(username); webrtcStartMessage1.setWebrtcusername(username);
webrtcStartMessage1.setWebrtcpassword(password); webrtcStartMessage1.setWebrtcpassword(password);
} }
@ -426,16 +437,16 @@ export class SocketManager {
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
//if (!user.socket.disconnecting) { //if (!user.socket.disconnecting) {
user.socket.write(serverToClientMessage1); user.socket.write(serverToClientMessage1);
//console.log('Sending webrtcstart initiator to '+user.socket.userId) //console.log('Sending webrtcstart initiator to '+user.socket.userId)
//} //}
const webrtcStartMessage2 = new WebRtcStartMessage(); const webrtcStartMessage2 = new WebRtcStartMessage();
webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setUserid(user.id);
webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setName(user.name);
webrtcStartMessage2.setInitiator(false); webrtcStartMessage2.setInitiator(false);
if (TURN_STATIC_AUTH_SECRET !== '') { if (TURN_STATIC_AUTH_SECRET !== "") {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
webrtcStartMessage2.setWebrtcusername(username); webrtcStartMessage2.setWebrtcusername(username);
webrtcStartMessage2.setWebrtcpassword(password); webrtcStartMessage2.setWebrtcpassword(password);
} }
@ -444,10 +455,9 @@ export class SocketManager {
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
//if (!otherUser.socket.disconnecting) { //if (!otherUser.socket.disconnecting) {
otherUser.socket.write(serverToClientMessage2); otherUser.socket.write(serverToClientMessage2);
//console.log('Sending webrtcstart to '+otherUser.socket.userId) //console.log('Sending webrtcstart to '+otherUser.socket.userId)
//} //}
} }
} }
@ -456,17 +466,17 @@ export class SocketManager {
* and the Coturn server. * and the Coturn server.
* The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey` * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
*/ */
private getTURNCredentials(name: string, secret: string): {username: string, password: string} { private getTURNCredentials(name: string, secret: string): { username: string; password: string } {
const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours
const username = [unixTimeStamp, name].join(':'); const username = [unixTimeStamp, name].join(":");
const hmac = crypto.createHmac('sha1', secret); const hmac = crypto.createHmac("sha1", secret);
hmac.setEncoding('base64'); hmac.setEncoding("base64");
hmac.write(username); hmac.write(username);
hmac.end(); hmac.end();
const password = hmac.read(); const password = hmac.read();
return { return {
username: username, username: username,
password: password password: password,
}; };
} }
@ -489,10 +499,9 @@ export class SocketManager {
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
//if (!otherUser.socket.disconnecting) { //if (!otherUser.socket.disconnecting) {
otherUser.socket.write(serverToClientMessage1); otherUser.socket.write(serverToClientMessage1);
//} //}
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
webrtcDisconnectMessage2.setUserid(otherUser.id); webrtcDisconnectMessage2.setUserid(otherUser.id);
@ -500,7 +509,7 @@ export class SocketManager {
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
//if (!user.socket.disconnecting) { //if (!user.socket.disconnecting) {
user.socket.write(serverToClientMessage2); user.socket.write(serverToClientMessage2);
//} //}
} }
} }
@ -517,40 +526,41 @@ export class SocketManager {
console.error('An error occurred on "emitPlayGlobalMessage" event'); console.error('An error occurred on "emitPlayGlobalMessage" event');
console.error(e); console.error(e);
} }
} }
public getWorlds(): Map<string, GameRoom> { public getWorlds(): Map<string, GameRoom> {
return this.rooms; return this.rooms;
} }
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) { public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
const room = queryJitsiJwtMessage.getJitsiroom(); const room = queryJitsiJwtMessage.getJitsiroom();
const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead. const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead.
if (SECRET_JITSI_KEY === '') { if (SECRET_JITSI_KEY === "") {
throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.");
} }
// Let's see if the current client has // Let's see if the current client has
const isAdmin = user.tags.includes(tag); const isAdmin = user.tags.includes(tag);
const jwt = Jwt.sign({ const jwt = Jwt.sign(
"aud": "jitsi", {
"iss": JITSI_ISS, aud: "jitsi",
"sub": JITSI_URL, iss: JITSI_ISS,
"room": room, sub: JITSI_URL,
"moderator": isAdmin room: room,
}, SECRET_JITSI_KEY, { moderator: isAdmin,
expiresIn: '1d', },
algorithm: "HS256", SECRET_JITSI_KEY,
header: {
{ expiresIn: "1d",
"alg": "HS256", algorithm: "HS256",
"typ": "JWT" header: {
} alg: "HS256",
}); typ: "JWT",
},
}
);
const sendJitsiJwtMessage = new SendJitsiJwtMessage(); const sendJitsiJwtMessage = new SendJitsiJwtMessage();
sendJitsiJwtMessage.setJitsiroom(room); sendJitsiJwtMessage.setJitsiroom(room);
@ -562,7 +572,7 @@ export class SocketManager {
user.socket.write(serverToClientMessage); user.socket.write(serverToClientMessage);
} }
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){ public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(sendUserMessageToSend.getMessage()); sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
sendUserMessage.setType(sendUserMessageToSend.getType()); sendUserMessage.setType(sendUserMessageToSend.getType());
@ -572,7 +582,7 @@ export class SocketManager {
user.socket.write(serverToClientMessage); user.socket.write(serverToClientMessage);
} }
public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){ public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) {
const banUserMessage = new BanUserMessage(); const banUserMessage = new BanUserMessage();
banUserMessage.setMessage(banUserMessageToSend.getMessage()); banUserMessage.setMessage(banUserMessageToSend.getMessage());
banUserMessage.setType(banUserMessageToSend.getType()); banUserMessage.setType(banUserMessageToSend.getType());
@ -592,7 +602,7 @@ export class SocketManager {
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void { public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
console.error("In addZoneListener, could not find room with id '" + roomId + "'"); console.error("In addZoneListener, could not find room with id '" + roomId + "'");
return; return;
} }
@ -636,7 +646,7 @@ export class SocketManager {
removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) { removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
console.error("In removeZoneListener, could not find room with id '" + roomId + "'"); console.error("In removeZoneListener, could not find room with id '" + roomId + "'");
return; return;
} }
@ -651,7 +661,7 @@ export class SocketManager {
return room; return room;
} }
public leaveAdminRoom(room: GameRoom, admin: Admin){ public leaveAdminRoom(room: GameRoom, admin: Admin) {
room.adminLeave(admin); room.adminLeave(admin);
if (room.isEmpty()) { if (room.isEmpty()) {
this.rooms.delete(room.roomId); this.rooms.delete(room.roomId);
@ -663,19 +673,27 @@ export class SocketManager {
public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void { public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
console.error("In sendAdminMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); console.error(
"In sendAdminMessage, could not find room with id '" +
roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
);
return; return;
} }
const recipient = room.getUserByUuid(recipientUuid); const recipient = room.getUserByUuid(recipientUuid);
if (recipient === undefined) { if (recipient === undefined) {
console.error("In sendAdminMessage, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); console.error(
"In sendAdminMessage, could not find user with id '" +
recipientUuid +
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
);
return; return;
} }
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message); sendUserMessage.setMessage(message);
sendUserMessage.setType('ban'); //todo: is the type correct? sendUserMessage.setType("ban"); //todo: is the type correct?
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSendusermessage(sendUserMessage); serverToClientMessage.setSendusermessage(sendUserMessage);
@ -686,13 +704,21 @@ export class SocketManager {
public banUser(roomId: string, recipientUuid: string, message: string): void { public banUser(roomId: string, recipientUuid: string, message: string): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
console.error("In banUser, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); console.error(
"In banUser, could not find room with id '" +
roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
);
return; return;
} }
const recipient = room.getUserByUuid(recipientUuid); const recipient = room.getUserByUuid(recipientUuid);
if (recipient === undefined) { if (recipient === undefined) {
console.error("In banUser, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); console.error(
"In banUser, could not find user with id '" +
recipientUuid +
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
);
return; return;
} }
@ -701,7 +727,7 @@ export class SocketManager {
const banUserMessage = new BanUserMessage(); const banUserMessage = new BanUserMessage();
banUserMessage.setMessage(message); banUserMessage.setMessage(message);
banUserMessage.setType('banned'); banUserMessage.setType("banned");
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setBanusermessage(banUserMessage); serverToClientMessage.setBanusermessage(banUserMessage);
@ -711,19 +737,22 @@ export class SocketManager {
recipient.socket.end(); recipient.socket.end();
} }
sendAdminRoomMessage(roomId: string, message: string) { sendAdminRoomMessage(roomId: string, message: string) {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
//todo: this should cause the http call to return a 500 //todo: this should cause the http call to return a 500
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); console.error(
"In sendAdminRoomMessage, could not find room with id '" +
roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
);
return; return;
} }
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message); sendUserMessage.setMessage(message);
sendUserMessage.setType('message'); sendUserMessage.setType("message");
const clientMessage = new ServerToClientMessage(); const clientMessage = new ServerToClientMessage();
clientMessage.setSendusermessage(sendUserMessage); clientMessage.setSendusermessage(sendUserMessage);
@ -732,14 +761,18 @@ export class SocketManager {
}); });
} }
dispatchWorlFullWarning(roomId: string,): void { dispatchWorlFullWarning(roomId: string): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
//todo: this should cause the http call to return a 500 //todo: this should cause the http call to return a 500
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); console.error(
"In sendAdminRoomMessage, could not find room with id '" +
roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
);
return; return;
} }
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const worldFullMessage = new WorldFullWarningMessage(); const worldFullMessage = new WorldFullWarningMessage();
@ -750,17 +783,17 @@ export class SocketManager {
}); });
} }
dispatchRoomRefresh(roomId: string,): void { dispatchRoomRefresh(roomId: string): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
if (!room) { if (!room) {
return; return;
} }
const versionNumber = room.incrementVersion(); const versionNumber = room.incrementVersion();
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const worldFullMessage = new RefreshRoomMessage(); const worldFullMessage = new RefreshRoomMessage();
worldFullMessage.setRoomid(roomId) worldFullMessage.setRoomid(roomId);
worldFullMessage.setVersionnumber(versionNumber) worldFullMessage.setVersionnumber(versionNumber);
const clientMessage = new ServerToClientMessage(); const clientMessage = new ServerToClientMessage();
clientMessage.setRefreshroommessage(worldFullMessage); clientMessage.setRefreshroommessage(worldFullMessage);

View File

@ -1,11 +1,11 @@
// lib/app.ts // lib/app.ts
import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..." import { IoSocketController } from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..."
import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..." import { AuthenticateController } from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..."
import {MapController} from "./Controller/MapController"; import { MapController } from "./Controller/MapController";
import {PrometheusController} from "./Controller/PrometheusController"; import { PrometheusController } from "./Controller/PrometheusController";
import {DebugController} from "./Controller/DebugController"; import { DebugController } from "./Controller/DebugController";
import {App as uwsApp} from "./Server/sifrr.server"; import { App as uwsApp } from "./Server/sifrr.server";
import {AdminController} from "./Controller/AdminController"; import { AdminController } from "./Controller/AdminController";
class App { class App {
public app: uwsApp; public app: uwsApp;

View File

@ -1,19 +1,21 @@
import {BaseController} from "./BaseController"; import { BaseController } from "./BaseController";
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
import {apiClientRepository} from "../Services/ApiClientRepository"; import { apiClientRepository } from "../Services/ApiClientRepository";
import {AdminRoomMessage, WorldFullWarningToRoomMessage, RefreshRoomPromptMessage} from "../Messages/generated/messages_pb"; import {
AdminRoomMessage,
WorldFullWarningToRoomMessage,
RefreshRoomPromptMessage,
} from "../Messages/generated/messages_pb";
export class AdminController extends BaseController {
export class AdminController extends BaseController{ constructor(private App: TemplatedApp) {
constructor(private App : TemplatedApp) {
super(); super();
this.App = App; this.App = App;
this.receiveGlobalMessagePrompt(); this.receiveGlobalMessagePrompt();
this.receiveRoomEditionPrompt(); this.receiveRoomEditionPrompt();
} }
receiveRoomEditionPrompt() { receiveRoomEditionPrompt() {
this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => { this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res); this.addCorsHeaders(res);
@ -23,25 +25,25 @@ export class AdminController extends BaseController{
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => { this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => { res.onAborted(() => {
console.warn('/message request was aborted'); console.warn("/message request was aborted");
}) });
const token = req.getHeader('admin-token'); const token = req.getHeader("admin-token");
const body = await res.json(); const body = await res.json();
if (token !== ADMIN_API_TOKEN) { if (token !== ADMIN_API_TOKEN) {
console.error('Admin access refused for token: '+token) console.error("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end('Incorrect token'); res.writeStatus("401 Unauthorized").end("Incorrect token");
return; return;
} }
try { try {
if (typeof body.roomId !== 'string') { if (typeof body.roomId !== "string") {
throw 'Incorrect roomId parameter' throw "Incorrect roomId parameter";
} }
const roomId: string = body.roomId; const roomId: string = body.roomId;
await apiClientRepository.getClient(roomId).then((roomClient) =>{ await apiClientRepository.getClient(roomId).then((roomClient) => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
const roomMessage = new RefreshRoomPromptMessage(); const roomMessage = new RefreshRoomPromptMessage();
roomMessage.setRoomid(roomId); roomMessage.setRoomid(roomId);
@ -57,12 +59,10 @@ export class AdminController extends BaseController{
} }
res.writeStatus("200"); res.writeStatus("200");
res.end('ok'); res.end("ok");
}); });
} }
receiveGlobalMessagePrompt() { receiveGlobalMessagePrompt() {
this.App.options("/message", (res: HttpResponse, req: HttpRequest) => { this.App.options("/message", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res); this.addCorsHeaders(res);
@ -71,59 +71,57 @@ export class AdminController extends BaseController{
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => { this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => { res.onAborted(() => {
console.warn('/message request was aborted'); console.warn("/message request was aborted");
}) });
const token = req.getHeader("admin-token");
const token = req.getHeader('admin-token');
const body = await res.json(); const body = await res.json();
if (token !== ADMIN_API_TOKEN) { if (token !== ADMIN_API_TOKEN) {
console.error('Admin access refused for token: '+token) console.error("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end('Incorrect token'); res.writeStatus("401 Unauthorized").end("Incorrect token");
return; return;
} }
try { try {
if (typeof body.text !== 'string') { if (typeof body.text !== "string") {
throw 'Incorrect text parameter' throw "Incorrect text parameter";
} }
if (body.type !== 'capacity' && body.type !== 'message') { if (body.type !== "capacity" && body.type !== "message") {
throw 'Incorrect type parameter' throw "Incorrect type parameter";
} }
if (!body.targets || typeof body.targets !== 'object') { if (!body.targets || typeof body.targets !== "object") {
throw 'Incorrect targets parameter' throw "Incorrect targets parameter";
} }
const text: string = body.text; const text: string = body.text;
const type: string = body.type; const type: string = body.type;
const targets: string[] = body.targets; const targets: string[] = body.targets;
await Promise.all(targets.map((roomId) => { await Promise.all(
return apiClientRepository.getClient(roomId).then((roomClient) =>{ targets.map((roomId) => {
return new Promise((res, rej) => { return apiClientRepository.getClient(roomId).then((roomClient) => {
if (type === 'message') { return new Promise((res, rej) => {
const roomMessage = new AdminRoomMessage(); if (type === "message") {
roomMessage.setMessage(text); const roomMessage = new AdminRoomMessage();
roomMessage.setRoomid(roomId); roomMessage.setMessage(text);
roomMessage.setRoomid(roomId);
roomClient.sendAdminMessageToRoom(roomMessage, (err) => { roomClient.sendAdminMessageToRoom(roomMessage, (err) => {
err ? rej(err) : res(); err ? rej(err) : res();
}); });
} else if (type === 'capacity') { } else if (type === "capacity") {
const roomMessage = new WorldFullWarningToRoomMessage(); const roomMessage = new WorldFullWarningToRoomMessage();
roomMessage.setRoomid(roomId); roomMessage.setRoomid(roomId);
roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => {
err ? rej(err) : res();
});
}
roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => {
err ? rej(err) : res();
});
}
});
}); });
}); })
})); );
} catch (err) { } catch (err) {
this.errorToResponse(err, res); this.errorToResponse(err, res);
return; return;
@ -131,7 +129,7 @@ export class AdminController extends BaseController{
res.writeStatus("200"); res.writeStatus("200");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end('ok'); res.end("ok");
}); });
} }
} }

View File

@ -1,17 +1,16 @@
import { v4 } from 'uuid'; import { v4 } from "uuid";
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import {BaseController} from "./BaseController"; import { BaseController } from "./BaseController";
import {adminApi} from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
import {jwtTokenManager} from "../Services/JWTTokenManager"; import { jwtTokenManager } from "../Services/JWTTokenManager";
import {parse} from "query-string"; import { parse } from "query-string";
export interface TokenInterface { export interface TokenInterface {
userUuid: string userUuid: string;
} }
export class AuthenticateController extends BaseController { export class AuthenticateController extends BaseController {
constructor(private App: TemplatedApp) {
constructor(private App : TemplatedApp) {
super(); super();
this.register(); this.register();
this.verify(); this.verify();
@ -19,7 +18,7 @@ export class AuthenticateController extends BaseController {
} }
//Try to login with an admin token //Try to login with an admin token
private register(){ private register() {
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res); this.addCorsHeaders(res);
@ -29,15 +28,15 @@ export class AuthenticateController extends BaseController {
this.App.post("/register", (res: HttpResponse, req: HttpRequest) => { this.App.post("/register", (res: HttpResponse, req: HttpRequest) => {
(async () => { (async () => {
res.onAborted(() => { res.onAborted(() => {
console.warn('Login request was aborted'); console.warn("Login request was aborted");
}) });
const param = await res.json(); const param = await res.json();
//todo: what to do if the organizationMemberToken is already used? //todo: what to do if the organizationMemberToken is already used?
const organizationMemberToken:string|null = param.organizationMemberToken; const organizationMemberToken: string | null = param.organizationMemberToken;
try { try {
if (typeof organizationMemberToken != 'string') throw new Error('No organization token'); if (typeof organizationMemberToken != "string") throw new Error("No organization token");
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
const userUuid = data.userUuid; const userUuid = data.userUuid;
const organizationSlug = data.organizationSlug; const organizationSlug = data.organizationSlug;
@ -49,28 +48,26 @@ export class AuthenticateController extends BaseController {
const authToken = jwtTokenManager.createJWTToken(userUuid); const authToken = jwtTokenManager.createJWTToken(userUuid);
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end(JSON.stringify({ res.end(
authToken, JSON.stringify({
userUuid, authToken,
organizationSlug, userUuid,
worldSlug, organizationSlug,
roomSlug, worldSlug,
mapUrlStart, roomSlug,
organizationMemberToken, mapUrlStart,
textures organizationMemberToken,
})); textures,
})
);
} catch (e) { } catch (e) {
this.errorToResponse(e, res); this.errorToResponse(e, res);
} }
})(); })();
}); });
} }
private verify(){ private verify() {
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => { this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res); this.addCorsHeaders(res);
@ -82,50 +79,55 @@ export class AuthenticateController extends BaseController {
const query = parse(req.getQuery()); const query = parse(req.getQuery());
res.onAborted(() => { res.onAborted(() => {
console.warn('verify request was aborted'); console.warn("verify request was aborted");
}) });
try { try {
await jwtTokenManager.getUserUuidFromToken(query.token as string); await jwtTokenManager.getUserUuidFromToken(query.token as string);
} catch (e) { } catch (e) {
res.writeStatus("400 Bad Request"); res.writeStatus("400 Bad Request");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end(JSON.stringify({ res.end(
"success": false, JSON.stringify({
"message": "Invalid JWT token" success: false,
})); message: "Invalid JWT token",
})
);
return; return;
} }
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end(JSON.stringify({ res.end(
"success": true JSON.stringify({
})); success: true,
})
);
})(); })();
}); });
} }
//permit to login on application. Return token to connect on Websocket IO. //permit to login on application. Return token to connect on Websocket IO.
private anonymLogin(){ private anonymLogin() {
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end(); res.end();
}); });
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => { res.onAborted(() => {
console.warn('Login request was aborted'); console.warn("Login request was aborted");
}) });
const userUuid = v4(); const userUuid = v4();
const authToken = jwtTokenManager.createJWTToken(userUuid); const authToken = jwtTokenManager.createJWTToken(userUuid);
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end(JSON.stringify({ res.end(
authToken, JSON.stringify({
userUuid, authToken,
})); userUuid,
})
);
}); });
} }
} }

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,23 +15,23 @@ export class BaseController {
if (e && e.message) { if (e && e.message) {
let url = e?.config?.url; let url = e?.config?.url;
if (url !== undefined) { if (url !== undefined) {
url = ' for URL: '+url; url = " for URL: " + url;
} else { } else {
url = ''; url = "";
} }
console.error('ERROR: '+e.message+url); console.error("ERROR: " + e.message + url);
} else if (typeof(e) === 'string') { } else if (typeof e === "string") {
console.error(e); console.error(e);
} }
if (e.stack) { if (e.stack) {
console.error(e.stack); console.error(e.stack);
} }
if (e.response) { if (e.response) {
res.writeStatus(e.response.status+" "+e.response.statusText); res.writeStatus(e.response.status + " " + e.response.statusText);
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end("An error occurred: "+e.response.status+" "+e.response.statusText); res.end("An error occurred: " + e.response.status + " " + e.response.statusText);
} else { } else {
res.writeStatus("500 Internal Server Error") res.writeStatus("500 Internal Server Error");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end("An error occurred"); res.end("An error occurred");
} }

View File

@ -1,45 +1,46 @@
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
import {IoSocketController} from "_Controller/IoSocketController"; import { IoSocketController } from "_Controller/IoSocketController";
import {stringify} from "circular-json"; import { stringify } from "circular-json";
import {HttpRequest, HttpResponse} from "uWebSockets.js"; import { HttpRequest, HttpResponse } from "uWebSockets.js";
import { parse } from 'query-string'; import { parse } from "query-string";
import {App} from "../Server/sifrr.server"; import { App } from "../Server/sifrr.server";
import {socketManager} from "../Services/SocketManager"; import { socketManager } from "../Services/SocketManager";
export class DebugController { export class DebugController {
constructor(private App : App) { constructor(private App: App) {
this.getDump(); this.getDump();
} }
getDump() {
getDump(){
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
const query = parse(req.getQuery()); const query = parse(req.getQuery());
if (query.token !== ADMIN_API_TOKEN) { if (query.token !== ADMIN_API_TOKEN) {
return res.status(401).send('Invalid token sent!'); return res.status(401).send("Invalid token sent!");
} }
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( return res
socketManager.getWorlds(), .writeStatus("200 OK")
(key: unknown, value: unknown) => { .writeHeader("Content-Type", "application/json")
if(value instanceof Map) { .end(
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
for (const [mapKey, mapValue] of value.entries()) { if (value instanceof Map) {
obj[mapKey] = mapValue; const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
} for (const [mapKey, mapValue] of value.entries()) {
return obj; obj[mapKey] = mapValue;
} else if(value instanceof Set) { }
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = []; const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) { for (const [setKey, setValue] of value.entries()) {
obj.push(setValue); obj.push(setValue);
} }
return obj; return obj;
} else { } else {
return value; return value;
} }
} })
)); );
}); });
} }
} }

View File

@ -1,6 +1,6 @@
import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
import {GameRoomPolicyTypes} from "../Model/PusherRoom"; import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import {PointInterface} from "../Model/Websocket/PointInterface"; import { PointInterface } from "../Model/Websocket/PointInterface";
import { import {
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SubMessage, SubMessage,
@ -18,17 +18,17 @@ import {
CompanionMessage, CompanionMessage,
EmotePromptMessage, EmotePromptMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {UserMovesMessage} from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb";
import {TemplatedApp} from "uWebSockets.js" import { TemplatedApp } from "uWebSockets.js";
import {parse} from "query-string"; import { parse } from "query-string";
import {jwtTokenManager} from "../Services/JWTTokenManager"; import { jwtTokenManager } from "../Services/JWTTokenManager";
import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; import { adminApi, CharacterTexture, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import {SocketManager, socketManager} from "../Services/SocketManager"; import { SocketManager, socketManager } from "../Services/SocketManager";
import {emitInBatch} from "../Services/IoSocketHelpers"; import { emitInBatch } from "../Services/IoSocketHelpers";
import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
import {Zone} from "_Model/Zone"; import { Zone } from "_Model/Zone";
import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
import {v4} from "uuid"; import { v4 } from "uuid";
export class IoSocketController { export class IoSocketController {
private nextUserId: number = 1; private nextUserId: number = 1;
@ -39,32 +39,29 @@ export class IoSocketController {
} }
adminRoomSocket() { adminRoomSocket() {
this.app.ws('/admin/rooms', { this.app.ws("/admin/rooms", {
upgrade: (res, req, context) => { upgrade: (res, req, context) => {
const query = parse(req.getQuery()); const query = parse(req.getQuery());
const websocketKey = req.getHeader('sec-websocket-key'); const websocketKey = req.getHeader("sec-websocket-key");
const websocketProtocol = req.getHeader('sec-websocket-protocol'); const websocketProtocol = req.getHeader("sec-websocket-protocol");
const websocketExtensions = req.getHeader('sec-websocket-extensions'); const websocketExtensions = req.getHeader("sec-websocket-extensions");
const token = query.token; const token = query.token;
if (token !== ADMIN_API_TOKEN) { if (token !== ADMIN_API_TOKEN) {
console.log('Admin access refused for token: '+token) console.log("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end('Incorrect token'); res.writeStatus("401 Unauthorized").end("Incorrect token");
return; return;
} }
const roomId = query.roomId; const roomId = query.roomId;
if (typeof roomId !== 'string') { if (typeof roomId !== "string") {
console.error('Received') console.error("Received");
res.writeStatus("400 Bad Request").end('Missing room id'); res.writeStatus("400 Bad Request").end("Missing room id");
return; return;
} }
res.upgrade( res.upgrade({ roomId }, websocketKey, websocketProtocol, websocketExtensions, context);
{roomId},
websocketKey, websocketProtocol, websocketExtensions, context,
);
}, },
open: (ws) => { open: (ws) => {
console.log('Admin socket connect for room: '+ws.roomId); console.log("Admin socket connect for room: " + ws.roomId);
ws.disconnecting = false; ws.disconnecting = false;
socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string); socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string);
@ -74,24 +71,34 @@ export class IoSocketController {
const roomId = ws.roomId as string; const roomId = ws.roomId as string;
//TODO refactor message type and data //TODO refactor message type and data
const message: {event: string, message: {type: string, message: unknown, userUuid: string}} = const message: { event: string; message: { type: string; message: unknown; userUuid: string } } =
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer))); JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
if(message.event === 'user-message') { if (message.event === "user-message") {
const messageToEmit = (message.message as { message: string, type: string, userUuid: string }); const messageToEmit = message.message as { message: string; type: string; userUuid: string };
if(messageToEmit.type === 'banned'){ if (messageToEmit.type === "banned") {
socketManager.emitBan(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string); socketManager.emitBan(
messageToEmit.userUuid,
messageToEmit.message,
messageToEmit.type,
ws.roomId as string
);
} }
if(messageToEmit.type === 'ban') { if (messageToEmit.type === "ban") {
socketManager.emitSendUserMessage(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string); socketManager.emitSendUserMessage(
messageToEmit.userUuid,
messageToEmit.message,
messageToEmit.type,
ws.roomId as string
);
} }
} }
}catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
}, },
close: (ws, code, message) => { close: (ws, code, message) => {
const Client = (ws as ExAdminSocketInterface); const Client = ws as ExAdminSocketInterface;
try { try {
Client.disconnecting = true; Client.disconnecting = true;
socketManager.leaveAdminRoom(Client); socketManager.leaveAdminRoom(Client);
@ -99,12 +106,12 @@ export class IoSocketController {
console.error('An error occurred on admin "disconnect"'); console.error('An error occurred on admin "disconnect"');
console.error(e); console.error(e);
} }
} },
}) });
} }
ioConnection() { ioConnection() {
this.app.ws('/room', { this.app.ws("/room", {
/* Options */ /* Options */
//compression: uWS.SHARED_COMPRESSOR, //compression: uWS.SHARED_COMPRESSOR,
idleTimeout: SOCKET_IDLE_TIMER, idleTimeout: SOCKET_IDLE_TIMER,
@ -114,7 +121,7 @@ export class IoSocketController {
upgrade: (res, req, context) => { upgrade: (res, req, context) => {
(async () => { (async () => {
/* Keep track of abortions */ /* Keep track of abortions */
const upgradeAborted = {aborted: false}; const upgradeAborted = { aborted: false };
res.onAborted(() => { res.onAborted(() => {
/* We can simply signal that we were aborted */ /* We can simply signal that we were aborted */
@ -123,15 +130,15 @@ export class IoSocketController {
const url = req.getUrl(); const url = req.getUrl();
const query = parse(req.getQuery()); const query = parse(req.getQuery());
const websocketKey = req.getHeader('sec-websocket-key'); const websocketKey = req.getHeader("sec-websocket-key");
const websocketProtocol = req.getHeader('sec-websocket-protocol'); const websocketProtocol = req.getHeader("sec-websocket-protocol");
const websocketExtensions = req.getHeader('sec-websocket-extensions'); const websocketExtensions = req.getHeader("sec-websocket-extensions");
const IPAddress = req.getHeader('x-forwarded-for'); const IPAddress = req.getHeader("x-forwarded-for");
const roomId = query.roomId; const roomId = query.roomId;
try { try {
if (typeof roomId !== 'string') { if (typeof roomId !== "string") {
throw new Error('Undefined room ID: '); throw new Error("Undefined room ID: ");
} }
const token = query.token; const token = query.token;
@ -143,62 +150,69 @@ export class IoSocketController {
const right = Number(query.right); const right = Number(query.right);
const name = query.name; const name = query.name;
let companion: CompanionMessage|undefined = undefined; let companion: CompanionMessage | undefined = undefined;
if (typeof query.companion === 'string') { if (typeof query.companion === "string") {
companion = new CompanionMessage(); companion = new CompanionMessage();
companion.setName(query.companion); companion.setName(query.companion);
} }
if (typeof name !== 'string') { if (typeof name !== "string") {
throw new Error('Expecting name'); throw new Error("Expecting name");
} }
if (name === '') { if (name === "") {
throw new Error('No empty name'); throw new Error("No empty name");
} }
let characterLayers = query.characterLayers; let characterLayers = query.characterLayers;
if (characterLayers === null) { if (characterLayers === null) {
throw new Error('Expecting skin'); throw new Error("Expecting skin");
} }
if (typeof characterLayers === 'string') { if (typeof characterLayers === "string") {
characterLayers = [ characterLayers ]; characterLayers = [characterLayers];
} }
const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId); const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId);
let memberTags: string[] = []; let memberTags: string[] = [];
let memberVisitCardUrl: string|null = null; let memberVisitCardUrl: string | null = null;
let memberMessages: unknown; let memberMessages: unknown;
let memberTextures: CharacterTexture[] = []; let memberTextures: CharacterTexture[] = [];
const room = await socketManager.getOrCreateRoom(roomId); const room = await socketManager.getOrCreateRoom(roomId);
if (ADMIN_API_URL) { if (ADMIN_API_URL) {
try { try {
let userData : FetchMemberDataByUuidResponse = { let userData: FetchMemberDataByUuidResponse = {
uuid: v4(), uuid: v4(),
tags: [], tags: [],
visitCardUrl: null, visitCardUrl: null,
textures: [], textures: [],
messages: [], messages: [],
anonymous: true anonymous: true,
}; };
try { try {
userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId); userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId);
}catch (err){ } catch (err) {
if (err?.response?.status == 404) { if (err?.response?.status == 404) {
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
console.warn('Cannot find user with uuid "'+userUuid+'". Performing an anonymous login instead.'); console.warn(
} else if(err?.response?.status == 403) { 'Cannot find user with uuid "' +
userUuid +
'". Performing an anonymous login instead.'
);
} else if (err?.response?.status == 403) {
// If we get an HTTP 403, the world is full. We need to broadcast a special error to the client. // If we get an HTTP 403, the world is full. We need to broadcast a special error to the client.
// we finish immediately the upgrade then we will close the socket as soon as it starts opening. // we finish immediately the upgrade then we will close the socket as soon as it starts opening.
return res.upgrade({ return res.upgrade(
rejected: true, {
message: err?.response?.data.message, rejected: true,
status: err?.response?.status message: err?.response?.data.message,
}, websocketKey, status: err?.response?.status,
websocketProtocol, },
websocketExtensions, websocketKey,
context); websocketProtocol,
}else{ websocketExtensions,
context
);
} else {
throw err; throw err;
} }
} }
@ -206,21 +220,30 @@ export class IoSocketController {
memberTags = userData.tags; memberTags = userData.tags;
memberVisitCardUrl = userData.visitCardUrl; memberVisitCardUrl = userData.visitCardUrl;
memberTextures = userData.textures; memberTextures = userData.textures;
if (!room.public && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags))) { if (
throw new Error('Insufficient privileges to access this room') !room.public &&
room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY &&
(userData.anonymous === true || !room.canAccess(memberTags))
) {
throw new Error("Insufficient privileges to access this room");
} }
if (!room.public && room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && userData.anonymous === true) { if (
throw new Error('Use the login URL to connect') !room.public &&
room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY &&
userData.anonymous === true
) {
throw new Error("Use the login URL to connect");
} }
} catch (e) { } catch (e) {
console.log('access not granted for user '+userUuid+' and room '+roomId); console.log("access not granted for user " + userUuid + " and room " + roomId);
console.error(e); console.error(e);
throw new Error('User cannot access this world') throw new Error("User cannot access this world");
} }
} }
// Generate characterLayers objects from characterLayers string[] // Generate characterLayers objects from characterLayers string[]
const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures); const characterLayerObjs: CharacterLayer[] =
SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
if (upgradeAborted.aborted) { if (upgradeAborted.aborted) {
console.log("Ouch! Client disconnected before we could upgrade it!"); console.log("Ouch! Client disconnected before we could upgrade it!");
@ -229,7 +252,8 @@ export class IoSocketController {
} }
/* This immediately calls open handler, you must not use res after this call */ /* This immediately calls open handler, you must not use res after this call */
res.upgrade({ res.upgrade(
{
// Data passed here is accessible on the "websocket" socket object. // Data passed here is accessible on the "websocket" socket object.
url, url,
token, token,
@ -246,22 +270,22 @@ export class IoSocketController {
position: { position: {
x: x, x: x,
y: y, y: y,
direction: 'down', direction: "down",
moving: false moving: false,
} as PointInterface, } as PointInterface,
viewport: { viewport: {
top, top,
right, right,
bottom, bottom,
left left,
} },
}, },
/* Spell these correctly */ /* Spell these correctly */
websocketKey, websocketKey,
websocketProtocol, websocketProtocol,
websocketExtensions, websocketExtensions,
context); context
);
} catch (e) { } catch (e) {
/*if (e instanceof Error) { /*if (e instanceof Error) {
console.log(e.message); console.log(e.message);
@ -269,23 +293,26 @@ export class IoSocketController {
} else { } else {
res.writeStatus("500 Internal Server Error").end('An error occurred'); res.writeStatus("500 Internal Server Error").end('An error occurred');
}*/ }*/
return res.upgrade({ return res.upgrade(
rejected: true, {
message: e.message ? e.message : '500 Internal Server Error' rejected: true,
}, websocketKey, message: e.message ? e.message : "500 Internal Server Error",
websocketProtocol, },
websocketExtensions, websocketKey,
context); websocketProtocol,
websocketExtensions,
context
);
} }
})(); })();
}, },
/* Handlers */ /* Handlers */
open: (ws) => { open: (ws) => {
if(ws.rejected === true) { if (ws.rejected === true) {
//FIX ME to use status code //FIX ME to use status code
if(ws.message === 'World is full'){ if (ws.message === "World is full") {
socketManager.emitWorldFullMessage(ws); socketManager.emitWorldFullMessage(ws);
}else{ } else {
socketManager.emitConnexionErrorMessage(ws, ws.message as string); socketManager.emitConnexionErrorMessage(ws, ws.message as string);
} }
ws.close(); ws.close();
@ -299,7 +326,7 @@ export class IoSocketController {
//get data information and show messages //get data information and show messages
if (client.messages && Array.isArray(client.messages)) { if (client.messages && Array.isArray(client.messages)) {
client.messages.forEach((c: unknown) => { client.messages.forEach((c: unknown) => {
const messageToSend = c as { type: string, message: string }; const messageToSend = c as { type: string; message: string };
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setType(messageToSend.type); sendUserMessage.setType(messageToSend.type);
@ -323,33 +350,48 @@ export class IoSocketController {
} else if (message.hasUsermovesmessage()) { } else if (message.hasUsermovesmessage()) {
socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
} else if (message.hasSetplayerdetailsmessage()) { } else if (message.hasSetplayerdetailsmessage()) {
socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage); socketManager.handleSetPlayerDetails(
client,
message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage
);
} else if (message.hasSilentmessage()) { } else if (message.hasSilentmessage()) {
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage); socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) { } else if (message.hasItemeventmessage()) {
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
} else if (message.hasWebrtcsignaltoservermessage()) { } else if (message.hasWebrtcsignaltoservermessage()) {
socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); socketManager.emitVideo(
client,
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) { } else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); socketManager.emitScreenSharing(
client,
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasPlayglobalmessage()) { } else if (message.hasPlayglobalmessage()) {
socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage); socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasReportplayermessage()){ } else if (message.hasReportplayermessage()) {
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
} else if (message.hasQueryjitsijwtmessage()){ } else if (message.hasQueryjitsijwtmessage()) {
socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); socketManager.handleQueryJitsiJwtMessage(
} else if (message.hasEmotepromptmessage()){ client,
socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage); message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
);
} else if (message.hasEmotepromptmessage()) {
socketManager.handleEmotePromptMessage(
client,
message.getEmotepromptmessage() as EmotePromptMessage
);
} }
/* Ok is false if backpressure was built up, wait for drain */ /* Ok is false if backpressure was built up, wait for drain */
//let ok = ws.send(message, isBinary); //let ok = ws.send(message, isBinary);
}, },
drain: (ws) => { drain: (ws) => {
console.log('WebSocket backpressure: ' + ws.getBufferedAmount()); console.log("WebSocket backpressure: " + ws.getBufferedAmount());
}, },
close: (ws, code, message) => { close: (ws, code, message) => {
const Client = (ws as ExSocketInterface); const Client = ws as ExSocketInterface;
try { try {
Client.disconnecting = true; Client.disconnecting = true;
//leave room //leave room
@ -358,13 +400,13 @@ export class IoSocketController {
console.error('An error occurred on "disconnect"'); console.error('An error occurred on "disconnect"');
console.error(e); console.error(e);
} }
} },
}) });
} }
//eslint-disable-next-line @typescript-eslint/no-explicit-any //eslint-disable-next-line @typescript-eslint/no-explicit-any
private initClient(ws: any): ExSocketInterface { private initClient(ws: any): ExSocketInterface {
const client : ExSocketInterface = ws; const client: ExSocketInterface = ws;
client.userId = this.nextUserId; client.userId = this.nextUserId;
this.nextUserId++; this.nextUserId++;
client.userUuid = ws.userUuid; client.userUuid = ws.userUuid;
@ -374,7 +416,7 @@ export class IoSocketController {
client.batchTimeout = null; client.batchTimeout = null;
client.emitInBatch = (payload: SubMessage): void => { client.emitInBatch = (payload: SubMessage): void => {
emitInBatch(client, payload); emitInBatch(client, payload);
} };
client.disconnecting = false; client.disconnecting = false;
client.messages = ws.messages; client.messages = ws.messages;

View File

@ -1,18 +1,15 @@
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import {BaseController} from "./BaseController"; import { BaseController } from "./BaseController";
import {parse} from "query-string"; import { parse } from "query-string";
import {adminApi} from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
export class MapController extends BaseController {
export class MapController extends BaseController{ constructor(private App: TemplatedApp) {
constructor(private App : TemplatedApp) {
super(); super();
this.App = App; this.App = App;
this.getMapUrl(); this.getMapUrl();
} }
// Returns a map mapping map name to file name of the map // Returns a map mapping map name to file name of the map
getMapUrl() { getMapUrl() {
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => { this.App.options("/map", (res: HttpResponse, req: HttpRequest) => {
@ -22,29 +19,28 @@ export class MapController extends BaseController{
}); });
this.App.get("/map", (res: HttpResponse, req: HttpRequest) => { this.App.get("/map", (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => { res.onAborted(() => {
console.warn('/map request was aborted'); console.warn("/map request was aborted");
}) });
const query = parse(req.getQuery()); const query = parse(req.getQuery());
if (typeof query.organizationSlug !== 'string') { if (typeof query.organizationSlug !== "string") {
console.error('Expected organizationSlug parameter'); console.error("Expected organizationSlug parameter");
res.writeStatus("400 Bad request"); res.writeStatus("400 Bad request");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end("Expected organizationSlug parameter"); res.end("Expected organizationSlug parameter");
return; return;
} }
if (typeof query.worldSlug !== 'string') { if (typeof query.worldSlug !== "string") {
console.error('Expected worldSlug parameter'); console.error("Expected worldSlug parameter");
res.writeStatus("400 Bad request"); res.writeStatus("400 Bad request");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end("Expected worldSlug parameter"); res.end("Expected worldSlug parameter");
return; return;
} }
if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { if (typeof query.roomSlug !== "string" && query.roomSlug !== undefined) {
console.error('Expected only one roomSlug parameter'); console.error("Expected only one roomSlug parameter");
res.writeStatus("400 Bad request"); res.writeStatus("400 Bad request");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end("Expected only one roomSlug parameter"); res.end("Expected only one roomSlug parameter");
@ -53,7 +49,11 @@ export class MapController extends BaseController{
(async () => { (async () => {
try { try {
const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined); const mapDetails = await adminApi.fetchMapDetails(
query.organizationSlug as string,
query.worldSlug as string,
query.roomSlug as string | undefined
);
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
@ -62,7 +62,6 @@ export class MapController extends BaseController{
this.errorToResponse(e, res); this.errorToResponse(e, res);
} }
})(); })();
}); });
} }
} }

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

@ -1,8 +1,8 @@
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
/** /**
* A physical object that can be placed into a Zone * A physical object that can be placed into a Zone
*/ */
export interface Movable { export interface Movable {
getPosition(): PositionInterface getPosition(): PositionInterface;
} }

View File

@ -8,9 +8,9 @@
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
* number of players around the current player. * number of players around the current player.
*/ */
import {Zone, ZoneEventListener} from "./Zone"; import { Zone, ZoneEventListener } from "./Zone";
import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
//import Debug from "debug"; //import Debug from "debug";
//const debug = Debug('positiondispatcher'); //const debug = Debug('positiondispatcher');
@ -21,19 +21,22 @@ interface ZoneDescriptor {
} }
export class PositionDispatcher { export class PositionDispatcher {
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
private zones: Zone[][] = []; private zones: Zone[][] = [];
constructor(public readonly roomId: string, private zoneWidth: number, private zoneHeight: number, private socketListener: ZoneEventListener) { constructor(
} public readonly roomId: string,
private zoneWidth: number,
private zoneHeight: number,
private socketListener: ZoneEventListener
) {}
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
return { return {
i: Math.floor(x / this.zoneWidth), i: Math.floor(x / this.zoneWidth),
j: Math.floor(y / this.zoneHeight), j: Math.floor(y / this.zoneHeight),
} };
} }
/** /**
@ -41,7 +44,7 @@ export class PositionDispatcher {
*/ */
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void { public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
if (viewport.left > viewport.right || viewport.top > viewport.bottom) { if (viewport.left > viewport.right || viewport.top > viewport.bottom) {
console.warn('Invalid viewport received: ', viewport); console.warn("Invalid viewport received: ", viewport);
return; return;
} }
@ -57,8 +60,8 @@ export class PositionDispatcher {
} }
} }
const addedZones = [...newZones].filter(x => !oldZones.has(x)); const addedZones = [...newZones].filter((x) => !oldZones.has(x));
const removedZones = [...oldZones].filter(x => !newZones.has(x)); const removedZones = [...oldZones].filter((x) => !newZones.has(x));
for (const zone of addedZones) { for (const zone of addedZones) {
zone.startListening(socket); zone.startListening(socket);

View File

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

View File

@ -1,9 +1,9 @@
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
import {PositionDispatcher} from "./PositionDispatcher"; import { PositionDispatcher } from "./PositionDispatcher";
import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
import {arrayIntersect} from "../Services/ArrayHelper"; import { arrayIntersect } from "../Services/ArrayHelper";
import {ZoneEventListener} from "_Model/Zone"; import { ZoneEventListener } from "_Model/Zone";
export enum GameRoomPolicyTypes { export enum GameRoomPolicyTypes {
ANONYMUS_POLICY = 1, ANONYMUS_POLICY = 1,
@ -17,13 +17,11 @@ export class PusherRoom {
public tags: string[]; public tags: string[];
public policyType: GameRoomPolicyTypes; public policyType: GameRoomPolicyTypes;
public readonly roomSlug: string; public readonly roomSlug: string;
public readonly worldSlug: string = ''; public readonly worldSlug: string = "";
public readonly organizationSlug: string = ''; public readonly organizationSlug: string = "";
private versionNumber: number = 1; private versionNumber: number = 1;
constructor(public readonly roomId: string, constructor(public readonly roomId: string, private socketListener: ZoneEventListener) {
private socketListener: ZoneEventListener)
{
this.public = isRoomAnonymous(roomId); this.public = isRoomAnonymous(roomId);
this.tags = []; this.tags = [];
this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY;
@ -31,7 +29,7 @@ export class PusherRoom {
if (this.public) { if (this.public) {
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
} else { } else {
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
this.roomSlug = roomSlug; this.roomSlug = roomSlug;
this.organizationSlug = organizationSlug; this.organizationSlug = organizationSlug;
this.worldSlug = worldSlug; this.worldSlug = worldSlug;
@ -41,11 +39,11 @@ export class PusherRoom {
this.positionNotifier = new PositionDispatcher(this.roomId, 320, 320, this.socketListener); this.positionNotifier = new PositionDispatcher(this.roomId, 320, 320, this.socketListener);
} }
public setViewport(socket : ExSocketInterface, viewport: ViewportInterface): void { public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
this.positionNotifier.setViewport(socket, viewport); this.positionNotifier.setViewport(socket, viewport);
} }
public leave(socket : ExSocketInterface){ public leave(socket: ExSocketInterface) {
this.positionNotifier.removeViewport(socket); this.positionNotifier.removeViewport(socket);
} }

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

@ -1,21 +1,22 @@
import {PointInterface} from "./PointInterface"; import { PointInterface } from "./PointInterface";
import {Identificable} from "./Identificable"; import { Identificable } from "./Identificable";
import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
import { import {
AdminPusherToBackMessage, AdminPusherToBackMessage,
BatchMessage, BatchMessage,
PusherToBackMessage, ServerToAdminClientMessage, PusherToBackMessage,
ServerToAdminClientMessage,
ServerToClientMessage, ServerToClientMessage,
SubMessage SubMessage,
} from "../../Messages/generated/messages_pb"; } from "../../Messages/generated/messages_pb";
import {WebSocket} from "uWebSockets.js" import { WebSocket } from "uWebSockets.js";
import {CharacterTexture} from "../../Services/AdminApi"; import { CharacterTexture } from "../../Services/AdminApi";
import {ClientDuplexStream} from "grpc"; import { ClientDuplexStream } from "grpc";
import {Zone} from "_Model/Zone"; import { Zone } from "_Model/Zone";
export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>; export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
export interface ExAdminSocketInterface extends WebSocket { export interface ExAdminSocketInterface extends WebSocket {
adminConnection: AdminConnection, adminConnection: AdminConnection;
disconnecting: boolean, disconnecting: boolean;
} }

View File

@ -1,23 +1,23 @@
import {PointInterface} from "./PointInterface"; import { PointInterface } from "./PointInterface";
import {Identificable} from "./Identificable"; import { Identificable } from "./Identificable";
import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
import { import {
BatchMessage, BatchMessage,
CompanionMessage, CompanionMessage,
PusherToBackMessage, PusherToBackMessage,
ServerToClientMessage, ServerToClientMessage,
SubMessage SubMessage,
} from "../../Messages/generated/messages_pb"; } from "../../Messages/generated/messages_pb";
import {WebSocket} from "uWebSockets.js" import { WebSocket } from "uWebSockets.js";
import {CharacterTexture} from "../../Services/AdminApi"; import { CharacterTexture } from "../../Services/AdminApi";
import {ClientDuplexStream} from "grpc"; import { ClientDuplexStream } from "grpc";
import {Zone} from "_Model/Zone"; import { Zone } from "_Model/Zone";
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>; export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
export interface CharacterLayer { export interface CharacterLayer {
name: string, name: string;
url: string|undefined url: string | undefined;
} }
export interface ExSocketInterface extends WebSocket, Identificable { export interface ExSocketInterface extends WebSocket, Identificable {
@ -36,12 +36,12 @@ export interface ExSocketInterface extends WebSocket, Identificable {
*/ */
emitInBatch: (payload: SubMessage) => void; emitInBatch: (payload: SubMessage) => void;
batchedMessages: BatchMessage; batchedMessages: BatchMessage;
batchTimeout: NodeJS.Timeout|null; batchTimeout: NodeJS.Timeout | null;
disconnecting: boolean, disconnecting: boolean;
messages: unknown, messages: unknown;
tags: string[], tags: string[];
visitCardUrl: string|null, visitCardUrl: string | null;
textures: CharacterTexture[], textures: CharacterTexture[];
backConnection: BackConnection, backConnection: BackConnection;
listenedZones: Set<Zone>; listenedZones: Set<Zone>;
} }

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

@ -1,34 +1,33 @@
import {PointInterface} from "./PointInterface"; import { PointInterface } from "./PointInterface";
import { import {
CharacterLayerMessage, CharacterLayerMessage,
ItemEventMessage, ItemEventMessage,
PointMessage, PointMessage,
PositionMessage PositionMessage,
} from "../../Messages/generated/messages_pb"; } from "../../Messages/generated/messages_pb";
import {CharacterLayer, ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import { CharacterLayer, ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
import {PositionInterface} from "_Model/PositionInterface"; import { PositionInterface } from "_Model/PositionInterface";
export class ProtobufUtils { export class ProtobufUtils {
public static toPositionMessage(point: PointInterface): PositionMessage { public static toPositionMessage(point: PointInterface): PositionMessage {
let direction: Direction; let direction: Direction;
switch (point.direction) { switch (point.direction) {
case 'up': case "up":
direction = Direction.UP; direction = Direction.UP;
break; break;
case 'down': case "down":
direction = Direction.DOWN; direction = Direction.DOWN;
break; break;
case 'left': case "left":
direction = Direction.LEFT; direction = Direction.LEFT;
break; break;
case 'right': case "right":
direction = Direction.RIGHT; direction = Direction.RIGHT;
break; break;
default: default:
throw new Error('unexpected direction'); throw new Error("unexpected direction");
} }
const position = new PositionMessage(); const position = new PositionMessage();
@ -44,16 +43,16 @@ export class ProtobufUtils {
let direction: string; let direction: string;
switch (position.getDirection()) { switch (position.getDirection()) {
case Direction.UP: case Direction.UP:
direction = 'up'; direction = "up";
break; break;
case Direction.DOWN: case Direction.DOWN:
direction = 'down'; direction = "down";
break; break;
case Direction.LEFT: case Direction.LEFT:
direction = 'left'; direction = "left";
break; break;
case Direction.RIGHT: case Direction.RIGHT:
direction = 'right'; direction = "right";
break; break;
default: default:
throw new Error("Unexpected direction"); throw new Error("Unexpected direction");
@ -82,7 +81,7 @@ export class ProtobufUtils {
event: itemEventMessage.getEvent(), event: itemEventMessage.getEvent(),
parameters: JSON.parse(itemEventMessage.getParametersjson()), parameters: JSON.parse(itemEventMessage.getParametersjson()),
state: JSON.parse(itemEventMessage.getStatejson()), state: JSON.parse(itemEventMessage.getStatejson()),
} };
} }
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
@ -96,7 +95,7 @@ export class ProtobufUtils {
} }
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
return characterLayers.map(function(characterLayer): CharacterLayerMessage { return characterLayers.map(function (characterLayer): CharacterLayerMessage {
const message = new CharacterLayerMessage(); const message = new CharacterLayerMessage();
message.setName(characterLayer.name); message.setName(characterLayer.name);
if (characterLayer.url) { if (characterLayer.url) {

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

@ -1,16 +1,23 @@
import {ExSocketInterface} from "./Websocket/ExSocketInterface"; import { ExSocketInterface } from "./Websocket/ExSocketInterface";
import {apiClientRepository} from "../Services/ApiClientRepository"; import { apiClientRepository } from "../Services/ApiClientRepository";
import { import {
BatchToPusherMessage, BatchToPusherMessage,
CharacterLayerMessage, GroupLeftZoneMessage, GroupUpdateMessage, GroupUpdateZoneMessage, CharacterLayerMessage,
PointMessage, PositionMessage, UserJoinedMessage, GroupLeftZoneMessage,
UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage, GroupUpdateMessage,
GroupUpdateZoneMessage,
PointMessage,
PositionMessage,
UserJoinedMessage,
UserJoinedZoneMessage,
UserLeftZoneMessage,
UserMovedMessage,
ZoneMessage, ZoneMessage,
EmoteEventMessage, EmoteEventMessage,
CompanionMessage CompanionMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {ClientReadableStream} from "grpc"; import { ClientReadableStream } from "grpc";
import {PositionDispatcher} from "_Model/PositionDispatcher"; import { PositionDispatcher } from "_Model/PositionDispatcher";
import Debug from "debug"; import Debug from "debug";
const debug = Debug("zone"); const debug = Debug("zone");
@ -30,24 +37,38 @@ export type MovesCallback = (thing: Movable, position: PositionInterface, listen
export type LeavesCallback = (thing: Movable, listener: User) => void;*/ export type LeavesCallback = (thing: Movable, listener: User) => void;*/
export class UserDescriptor { export class UserDescriptor {
private constructor(public readonly userId: number, private name: string, private characterLayers: CharacterLayerMessage[], private position: PositionMessage, private visitCardUrl: string | null, private companion?: CompanionMessage) { private constructor(
public readonly userId: number,
private name: string,
private characterLayers: CharacterLayerMessage[],
private position: PositionMessage,
private visitCardUrl: string | null,
private companion?: CompanionMessage
) {
if (!Number.isInteger(this.userId)) { if (!Number.isInteger(this.userId)) {
throw new Error('UserDescriptor.userId is not an integer: '+this.userId); throw new Error("UserDescriptor.userId is not an integer: " + this.userId);
} }
} }
public static createFromUserJoinedZoneMessage(message: UserJoinedZoneMessage): UserDescriptor { public static createFromUserJoinedZoneMessage(message: UserJoinedZoneMessage): UserDescriptor {
const position = message.getPosition(); const position = message.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Missing position'); throw new Error("Missing position");
} }
return new UserDescriptor(message.getUserid(), message.getName(), message.getCharacterlayersList(), position, message.getVisitcardurl(), message.getCompanion()); return new UserDescriptor(
message.getUserid(),
message.getName(),
message.getCharacterlayersList(),
position,
message.getVisitcardurl(),
message.getCompanion()
);
} }
public update(userMovedMessage: UserMovedMessage) { public update(userMovedMessage: UserMovedMessage) {
const position = userMovedMessage.getPosition(); const position = userMovedMessage.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Missing position'); throw new Error("Missing position");
} }
this.position = position; this.position = position;
} }
@ -78,13 +99,12 @@ export class UserDescriptor {
} }
export class GroupDescriptor { export class GroupDescriptor {
private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) { private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) {}
}
public static createFromGroupUpdateZoneMessage(message: GroupUpdateZoneMessage): GroupDescriptor { public static createFromGroupUpdateZoneMessage(message: GroupUpdateZoneMessage): GroupDescriptor {
const position = message.getPosition(); const position = message.getPosition();
if (position === undefined) { if (position === undefined) {
throw new Error('Missing position'); throw new Error("Missing position");
} }
return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position); return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position);
} }
@ -97,7 +117,7 @@ export class GroupDescriptor {
public toGroupUpdateMessage(): GroupUpdateMessage { public toGroupUpdateMessage(): GroupUpdateMessage {
const groupUpdateMessage = new GroupUpdateMessage(); const groupUpdateMessage = new GroupUpdateMessage();
if (!Number.isInteger(this.groupId)) { if (!Number.isInteger(this.groupId)) {
throw new Error('GroupDescriptor.groupId is not an integer: '+this.groupId); throw new Error("GroupDescriptor.groupId is not an integer: " + this.groupId);
} }
groupUpdateMessage.setGroupid(this.groupId); groupUpdateMessage.setGroupid(this.groupId);
groupUpdateMessage.setGroupsize(this.groupSize); groupUpdateMessage.setGroupsize(this.groupSize);
@ -108,8 +128,8 @@ export class GroupDescriptor {
} }
interface ZoneDescriptor { interface ZoneDescriptor {
x: number, x: number;
y: number y: number;
} }
export class Zone { export class Zone {
@ -120,21 +140,26 @@ export class Zone {
private backConnection!: ClientReadableStream<BatchToPusherMessage>; private backConnection!: ClientReadableStream<BatchToPusherMessage>;
private isClosing: boolean = false; private isClosing: boolean = false;
constructor(private positionDispatcher: PositionDispatcher, private socketListener: ZoneEventListener, public readonly x: number, public readonly y: number, private onBackFailure: (e: Error|null, zone: Zone) => void) { constructor(
} private positionDispatcher: PositionDispatcher,
private socketListener: ZoneEventListener,
public readonly x: number,
public readonly y: number,
private onBackFailure: (e: Error | null, zone: Zone) => void
) {}
/** /**
* Creates a connection to the back server to track the users. * Creates a connection to the back server to track the users.
*/ */
public async init(): Promise<void> { public async init(): Promise<void> {
debug('Opening connection to zone %d, %d on back server', this.x, this.y); debug("Opening connection to zone %d, %d on back server", this.x, this.y);
const apiClient = await apiClientRepository.getClient(this.positionDispatcher.roomId); const apiClient = await apiClientRepository.getClient(this.positionDispatcher.roomId);
const zoneMessage = new ZoneMessage(); const zoneMessage = new ZoneMessage();
zoneMessage.setRoomid(this.positionDispatcher.roomId); zoneMessage.setRoomid(this.positionDispatcher.roomId);
zoneMessage.setX(this.x); zoneMessage.setX(this.x);
zoneMessage.setY(this.y); zoneMessage.setY(this.y);
this.backConnection = apiClient.listenZone(zoneMessage); this.backConnection = apiClient.listenZone(zoneMessage);
this.backConnection.on('data', (batch: BatchToPusherMessage) => { this.backConnection.on("data", (batch: BatchToPusherMessage) => {
for (const message of batch.getPayloadList()) { for (const message of batch.getPayloadList()) {
if (message.hasUserjoinedzonemessage()) { if (message.hasUserjoinedzonemessage()) {
const userJoinedZoneMessage = message.getUserjoinedzonemessage() as UserJoinedZoneMessage; const userJoinedZoneMessage = message.getUserjoinedzonemessage() as UserJoinedZoneMessage;
@ -179,33 +204,32 @@ export class Zone {
const userDescriptor = this.users.get(userId); const userDescriptor = this.users.get(userId);
if (userDescriptor === undefined) { if (userDescriptor === undefined) {
console.error('Unexpected move message received for user "'+userId+'"'); console.error('Unexpected move message received for user "' + userId + '"');
return; return;
} }
userDescriptor.update(userMovedMessage); userDescriptor.update(userMovedMessage);
this.notifyUserMove(userDescriptor); this.notifyUserMove(userDescriptor);
} else if(message.hasEmoteeventmessage()) { } else if (message.hasEmoteeventmessage()) {
const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage;
this.notifyEmote(emoteEventMessage); this.notifyEmote(emoteEventMessage);
} else { } else {
throw new Error('Unexpected message'); throw new Error("Unexpected message");
} }
} }
}); });
this.backConnection.on('error', (e) => { this.backConnection.on("error", (e) => {
if (!this.isClosing) { if (!this.isClosing) {
debug('Error on back connection') debug("Error on back connection");
this.close(); this.close();
this.onBackFailure(e, this); this.onBackFailure(e, this);
} }
}); });
this.backConnection.on('close', () => { this.backConnection.on("close", () => {
if (!this.isClosing) { if (!this.isClosing) {
debug('Close on back connection') debug("Close on back connection");
this.close(); this.close();
this.onBackFailure(null, this); this.onBackFailure(null, this);
} }
@ -213,7 +237,7 @@ export class Zone {
} }
public close(): void { public close(): void {
debug('Closing connection to zone %d, %d on back server', this.x, this.y); debug("Closing connection to zone %d, %d on back server", this.x, this.y);
this.isClosing = true; this.isClosing = true;
this.backConnection.cancel(); this.backConnection.cancel();
} }
@ -225,7 +249,7 @@ export class Zone {
/** /**
* Notify listeners of this zone that this user entered * Notify listeners of this zone that this user entered
*/ */
private notifyUserEnter(user: UserDescriptor, oldZone: ZoneDescriptor|undefined) { private notifyUserEnter(user: UserDescriptor, oldZone: ZoneDescriptor | undefined) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
if (listener.userId === user.userId) { if (listener.userId === user.userId) {
continue; continue;
@ -241,7 +265,7 @@ export class Zone {
/** /**
* Notify listeners of this zone that this group entered * Notify listeners of this zone that this group entered
*/ */
private notifyGroupEnter(group: GroupDescriptor, oldZone: ZoneDescriptor|undefined) { private notifyGroupEnter(group: GroupDescriptor, oldZone: ZoneDescriptor | undefined) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
if (oldZone === undefined || !this.isListeningZone(listener, oldZone.x, oldZone.y)) { if (oldZone === undefined || !this.isListeningZone(listener, oldZone.x, oldZone.y)) {
this.socketListener.onGroupEnters(group, listener); this.socketListener.onGroupEnters(group, listener);
@ -254,7 +278,7 @@ export class Zone {
/** /**
* Notify listeners of this zone that this user left * Notify listeners of this zone that this user left
*/ */
private notifyUserLeft(userId: number, newZone: ZoneDescriptor|undefined) { private notifyUserLeft(userId: number, newZone: ZoneDescriptor | undefined) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
if (listener.userId === userId) { if (listener.userId === userId) {
continue; continue;
@ -279,7 +303,7 @@ export class Zone {
/** /**
* Notify listeners of this zone that this group left * Notify listeners of this zone that this group left
*/ */
private notifyGroupLeft(groupId: number, newZone: ZoneDescriptor|undefined) { private notifyGroupLeft(groupId: number, newZone: ZoneDescriptor | undefined) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
if (listener.groupId === groupId) { if (listener.groupId === groupId) {
continue; continue;

View File

@ -1,13 +1,13 @@
import { App as _App, AppOptions } from 'uWebSockets.js'; import { App as _App, AppOptions } from "uWebSockets.js";
import BaseApp from './baseapp'; import BaseApp from "./baseapp";
import { extend } from './utils'; import { extend } from "./utils";
import { UwsApp } from './types'; import { UwsApp } from "./types";
class App extends (<UwsApp>_App) { class App extends (<UwsApp>_App) {
constructor(options: AppOptions = {}) { constructor(options: AppOptions = {}) {
super(options); // eslint-disable-line constructor-super super(options); // eslint-disable-line constructor-super
extend(this, new BaseApp()); extend(this, new BaseApp());
} }
} }
export default App; export default App;

View File

@ -1,116 +1,109 @@
import { Readable } from 'stream'; import { Readable } from "stream";
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
import formData from './formdata'; import formData from "./formdata";
import { stob } from './utils'; import { stob } from "./utils";
import { Handler } from './types'; import { Handler } from "./types";
import {join} from "path"; import { join } from "path";
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
const noOp = () => true; const noOp = () => true;
const handleBody = (res: HttpResponse, req: HttpRequest) => { const handleBody = (res: HttpResponse, req: HttpRequest) => {
const contType = req.getHeader('content-type'); const contType = req.getHeader("content-type");
res.bodyStream = function() { res.bodyStream = function () {
const stream = new Readable(); const stream = new Readable();
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
this.onData((ab: ArrayBuffer, isLast: boolean) => { this.onData((ab: ArrayBuffer, isLast: boolean) => {
// uint and then slicing is bit faster than slice and then uint // uint and then slicing is bit faster than slice and then uint
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
if (isLast) { if (isLast) {
stream.push(null); stream.push(null);
} }
}); });
return stream; return stream;
}; };
res.body = () => stob(res.bodyStream()); res.body = () => stob(res.bodyStream());
if (contType.includes('application/json')) if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
res.json = async () => JSON.parse(await res.body()); if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
if (contTypes.map(t => contType.includes(t)).includes(true))
res.formData = formData.bind(res, contType);
}; };
class BaseApp { class BaseApp {
_sockets = new Map(); _sockets = new Map();
ws!: TemplatedApp['ws']; ws!: TemplatedApp["ws"];
get!: TemplatedApp['get']; get!: TemplatedApp["get"];
_post!: TemplatedApp['post']; _post!: TemplatedApp["post"];
_put!: TemplatedApp['put']; _put!: TemplatedApp["put"];
_patch!: TemplatedApp['patch']; _patch!: TemplatedApp["patch"];
_listen!: TemplatedApp['listen']; _listen!: TemplatedApp["listen"];
post(pattern: string, handler: Handler) { post(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._post(pattern, (res, req) => {
this._post(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req); handler(res, req);
handler(res, req); });
}); return this;
return this; }
}
put(pattern: string, handler: Handler) { put(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._put(pattern, (res, req) => {
this._put(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req);
handler(res, req); handler(res, req);
}); });
return this; return this;
} }
patch(pattern: string, handler: Handler) { patch(pattern: string, handler: Handler) {
if (typeof handler !== 'function') if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
throw Error(`handler should be a function, given ${typeof handler}.`); this._patch(pattern, (res, req) => {
this._patch(pattern, (res, req) => { handleBody(res, req);
handleBody(res, req);
handler(res, req); handler(res, req);
}); });
return this; return this;
} }
listen(h: string | number, p: Function | number = noOp, cb?: Function) { listen(h: string | number, p: Function | number = noOp, cb?: Function) {
if (typeof p === 'number' && typeof h === 'string') { if (typeof p === "number" && typeof h === "string") {
this._listen(h, p, socket => { this._listen(h, p, (socket) => {
this._sockets.set(p, socket); this._sockets.set(p, socket);
if (cb === undefined) { if (cb === undefined) {
throw new Error('cb undefined'); throw new Error("cb undefined");
}
cb(socket);
});
} else if (typeof h === "number" && typeof p === "function") {
this._listen(h, (socket) => {
this._sockets.set(h, socket);
p(socket);
});
} else {
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
} }
cb(socket);
}); return this;
} else if (typeof h === 'number' && typeof p === 'function') {
this._listen(h, socket => {
this._sockets.set(h, socket);
p(socket);
});
} else {
throw Error(
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
);
} }
return this; close(port: null | number = null) {
} if (port) {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
close(port: null | number = null) { this._sockets.delete(port);
if (port) { } else {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); this._sockets.forEach((app) => {
this._sockets.delete(port); us_listen_socket_close(app);
} else { });
this._sockets.forEach(app => { this._sockets.clear();
us_listen_socket_close(app); }
}); return this;
this._sockets.clear();
} }
return this;
}
} }
export default BaseApp; export default BaseApp;

View File

@ -1,100 +1,99 @@
import { createWriteStream } from 'fs'; import { createWriteStream } from "fs";
import { join, dirname } from 'path'; import { join, dirname } from "path";
import Busboy from 'busboy'; import Busboy from "busboy";
import mkdirp from 'mkdirp'; import mkdirp from "mkdirp";
function formData( function formData(
contType: string, contType: string,
options: busboy.BusboyConfig & { options: busboy.BusboyConfig & {
abortOnLimit?: boolean; abortOnLimit?: boolean;
tmpDir?: string; tmpDir?: string;
onFile?: ( onFile?: (
fieldname: string, fieldname: string,
file: NodeJS.ReadableStream, file: NodeJS.ReadableStream,
filename: string, filename: string,
encoding: string, encoding: string,
mimetype: string mimetype: string
) => string; ) => string;
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
filename?: (oldName: string) => string; filename?: (oldName: string) => string;
} = {} } = {}
) { ) {
console.log('Enter form data'); console.log("Enter form data");
options.headers = { options.headers = {
'content-type': contType "content-type": contType,
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const busb = new Busboy(options); const busb = new Busboy(options);
const ret = {}; const ret = {};
this.bodyStream().pipe(busb); this.bodyStream().pipe(busb);
busb.on('limit', () => { busb.on("limit", () => {
if (options.abortOnLimit) { if (options.abortOnLimit) {
reject(Error('limit')); reject(Error("limit"));
} }
});
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
filename,
encoding,
mimetype,
filePath: undefined,
};
if (typeof options.tmpDir === "string") {
if (typeof options.filename === "function") filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave;
}
if (typeof options.onFile === "function") {
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
}
setRetValue(ret, fieldname, value);
});
busb.on("field", function (fieldname, value) {
if (typeof options.onField === "function") options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on("finish", function () {
resolve(ret);
});
busb.on("error", reject);
}); });
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = {
filename,
encoding,
mimetype,
filePath: undefined
};
if (typeof options.tmpDir === 'string') {
if (typeof options.filename === 'function') filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave;
}
if (typeof options.onFile === 'function') {
value.filePath =
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
}
setRetValue(ret, fieldname, value);
});
busb.on('field', function(fieldname, value) {
if (typeof options.onField === 'function') options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on('finish', function() {
resolve(ret);
});
busb.on('error', reject);
});
} }
function setRetValue( function setRetValue(
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
fieldname: string, fieldname: string,
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
) { ) {
if (fieldname.endsWith('[]')) { if (fieldname.endsWith("[]")) {
fieldname = fieldname.slice(0, fieldname.length - 2); fieldname = fieldname.slice(0, fieldname.length - 2);
if (Array.isArray(ret[fieldname])) { if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value); ret[fieldname].push(value);
} else {
ret[fieldname] = [value];
}
} else { } else {
ret[fieldname] = [value]; if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else if (ret[fieldname]) {
ret[fieldname] = [ret[fieldname], value];
} else {
ret[fieldname] = value;
}
} }
} else {
if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else if (ret[fieldname]) {
ret[fieldname] = [ret[fieldname], value];
} else {
ret[fieldname] = value;
}
}
} }
export default formData; export default formData;

View File

@ -1,13 +1,13 @@
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
import BaseApp from './baseapp'; import BaseApp from "./baseapp";
import { extend } from './utils'; import { extend } from "./utils";
import { UwsApp } from './types'; import { UwsApp } from "./types";
class SSLApp extends (<UwsApp>_SSLApp) { class SSLApp extends (<UwsApp>_SSLApp) {
constructor(options: AppOptions) { constructor(options: AppOptions) {
super(options); // eslint-disable-line constructor-super super(options); // eslint-disable-line constructor-super
extend(this, new BaseApp()); extend(this, new BaseApp());
} }
} }
export default SSLApp; export default SSLApp;

View File

@ -1,9 +1,9 @@
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
export type UwsApp = { export type UwsApp = {
(options: AppOptions): TemplatedApp; (options: AppOptions): TemplatedApp;
new (options: AppOptions): TemplatedApp; new (options: AppOptions): TemplatedApp;
prototype: TemplatedApp; prototype: TemplatedApp;
}; };
export type Handler = (res: HttpResponse, req: HttpRequest) => void; export type Handler = (res: HttpResponse, req: HttpRequest) => void;

View File

@ -1,37 +1,36 @@
import { ReadStream } from 'fs'; import { ReadStream } from "fs";
function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( function extend(who: any, from: any, overwrite = true) {
Object.keys(from) const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
); ownProps.forEach((prop) => {
ownProps.forEach(prop => { if (prop === "constructor" || from[prop] === undefined) return;
if (prop === 'constructor' || from[prop] === undefined) return; if (who[prop] && overwrite) {
if (who[prop] && overwrite) { who[`_${prop}`] = who[prop];
who[`_${prop}`] = who[prop]; }
} if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); else who[prop] = from[prop];
else who[prop] = from[prop]; });
});
} }
function stob(stream: ReadStream): Promise<Buffer> { function stob(stream: ReadStream): Promise<Buffer> {
return new Promise(resolve => { return new Promise((resolve) => {
const buffers: Buffer[] = []; const buffers: Buffer[] = [];
stream.on('data', buffers.push.bind(buffers)); stream.on("data", buffers.push.bind(buffers));
stream.on('end', () => { stream.on("end", () => {
switch (buffers.length) { switch (buffers.length) {
case 0: case 0:
resolve(Buffer.allocUnsafe(0)); resolve(Buffer.allocUnsafe(0));
break; break;
case 1: case 1:
resolve(buffers[0]); resolve(buffers[0]);
break; break;
default: default:
resolve(Buffer.concat(buffers)); resolve(Buffer.concat(buffers));
} }
});
}); });
});
} }
export { extend, stob }; export { extend, stob };

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,123 +1,147 @@
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import Axios from "axios"; import Axios from "axios";
import {GameRoomPolicyTypes} from "_Model/PusherRoom"; import { GameRoomPolicyTypes } from "_Model/PusherRoom";
export interface AdminApiData { export interface AdminApiData {
organizationSlug: string organizationSlug: string;
worldSlug: string worldSlug: string;
roomSlug: string roomSlug: string;
mapUrlStart: string mapUrlStart: string;
tags: string[] tags: string[];
policy_type: number policy_type: number;
userUuid: string userUuid: string;
messages?: unknown[], messages?: unknown[];
textures: CharacterTexture[] textures: CharacterTexture[];
} }
export interface MapDetailsData { export interface MapDetailsData {
roomSlug: string, roomSlug: string;
mapUrl: string, mapUrl: string;
policy_type: GameRoomPolicyTypes, policy_type: GameRoomPolicyTypes;
tags: string[], tags: string[];
} }
export interface AdminBannedData { export interface AdminBannedData {
is_banned: boolean, is_banned: boolean;
message: string message: string;
} }
export interface CharacterTexture { export interface CharacterTexture {
id: number, id: number;
level: number, level: number;
url: string, url: string;
rights: string rights: string;
} }
export interface FetchMemberDataByUuidResponse { export interface FetchMemberDataByUuidResponse {
uuid: string; uuid: string;
tags: string[]; tags: string[];
visitCardUrl: string|null; visitCardUrl: string | null;
textures: CharacterTexture[]; textures: CharacterTexture[];
messages: unknown[]; messages: unknown[];
anonymous?: boolean; anonymous?: boolean;
} }
class AdminApi { class AdminApi {
async fetchMapDetails(
async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise<MapDetailsData> { organizationSlug: string,
worldSlug: string,
roomSlug: string | undefined
): Promise<MapDetailsData> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject(new Error('No admin backoffice set!')); return Promise.reject(new Error("No admin backoffice set!"));
} }
const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { const params: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
organizationSlug, organizationSlug,
worldSlug worldSlug,
}; };
if (roomSlug) { if (roomSlug) {
params.roomSlug = roomSlug; params.roomSlug = roomSlug;
} }
const res = await Axios.get(ADMIN_API_URL + '/api/map', const res = await Axios.get(ADMIN_API_URL + "/api/map", {
{ headers: { Authorization: `${ADMIN_API_TOKEN}` },
headers: {"Authorization": `${ADMIN_API_TOKEN}`}, params,
params });
}
)
return res.data; return res.data;
} }
async fetchMemberDataByUuid(uuid: string, roomId: string): Promise<FetchMemberDataByUuidResponse> { async fetchMemberDataByUuid(uuid: string, roomId: string): Promise<FetchMemberDataByUuidResponse> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject(new Error('No admin backoffice set!')); return Promise.reject(new Error("No admin backoffice set!"));
} }
const res = await Axios.get(ADMIN_API_URL+'/api/room/access', const res = await Axios.get(ADMIN_API_URL + "/api/room/access", {
{ params: {uuid, roomId}, headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } params: { uuid, roomId },
) headers: { Authorization: `${ADMIN_API_TOKEN}` },
});
return res.data; return res.data;
} }
async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> { async fetchMemberDataByToken(organizationMemberToken: string): Promise<AdminApiData> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject(new Error('No admin backoffice set!')); return Promise.reject(new Error("No admin backoffice set!"));
} }
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, {
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } headers: { Authorization: `${ADMIN_API_TOKEN}` },
) });
return res.data; return res.data;
} }
async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> { async fetchCheckUserByToken(organizationMemberToken: string): Promise<AdminApiData> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject(new Error('No admin backoffice set!')); return Promise.reject(new Error("No admin backoffice set!"));
} }
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken, const res = await Axios.get(ADMIN_API_URL + "/api/check-user/" + organizationMemberToken, {
{ headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } headers: { Authorization: `${ADMIN_API_TOKEN}` },
) });
return res.data; return res.data;
} }
reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) { reportPlayer(
return Axios.post(`${ADMIN_API_URL}/api/report`, { reportedUserUuid: string,
reportedUserComment: string,
reporterUserUuid: string,
reportWorldSlug: string
) {
return Axios.post(
`${ADMIN_API_URL}/api/report`,
{
reportedUserUuid, reportedUserUuid,
reportedUserComment, reportedUserComment,
reporterUserUuid, reporterUserUuid,
reportWorldSlug reportWorldSlug,
}, },
{ {
headers: {"Authorization": `${ADMIN_API_TOKEN}`} headers: { Authorization: `${ADMIN_API_TOKEN}` },
}); }
);
} }
async verifyBanUser(organizationMemberToken: string, ipAddress: string, organization: string, world: string): Promise<AdminBannedData> { async verifyBanUser(
organizationMemberToken: string,
ipAddress: string,
organization: string,
world: string
): Promise<AdminBannedData> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject(new Error('No admin backoffice set!')); return Promise.reject(new Error("No admin backoffice set!"));
} }
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
return Axios.get(ADMIN_API_URL + '/api/check-moderate-user/'+organization+'/'+world+'?ipAddress='+ipAddress+'&token='+organizationMemberToken, return Axios.get(
{headers: {"Authorization": `${ADMIN_API_TOKEN}`}} ADMIN_API_URL +
"/api/check-moderate-user/" +
organization +
"/" +
world +
"?ipAddress=" +
ipAddress +
"&token=" +
organizationMemberToken,
{ headers: { Authorization: `${ADMIN_API_TOKEN}` } }
).then((data) => { ).then((data) => {
return data.data; return data.data;
}); });

View File

@ -1,14 +1,14 @@
/** /**
* A class to get connections to the correct "api" server given a room name. * A class to get connections to the correct "api" server given a room name.
*/ */
import {RoomManagerClient} from "../Messages/generated/messages_grpc_pb"; import { RoomManagerClient } from "../Messages/generated/messages_grpc_pb";
import grpc from 'grpc'; import grpc from "grpc";
import crypto from 'crypto'; import crypto from "crypto";
import {API_URL} from "../Enum/EnvironmentVariable"; import { API_URL } from "../Enum/EnvironmentVariable";
import Debug from "debug"; import Debug from "debug";
const debug = Debug('apiClientRespository'); const debug = Debug("apiClientRespository");
class ApiClientRepository { class ApiClientRepository {
private roomManagerClients: RoomManagerClient[] = []; private roomManagerClients: RoomManagerClient[] = [];
@ -16,23 +16,26 @@ class ApiClientRepository {
public constructor(private apiUrls: string[]) {} public constructor(private apiUrls: string[]) {}
public async getClient(roomId: string): Promise<RoomManagerClient> { public async getClient(roomId: string): Promise<RoomManagerClient> {
const array = new Uint32Array(crypto.createHash('md5').update(roomId).digest()); const array = new Uint32Array(crypto.createHash("md5").update(roomId).digest());
const index = array[0] % this.apiUrls.length; const index = array[0] % this.apiUrls.length;
let client = this.roomManagerClients[index]; let client = this.roomManagerClients[index];
if (client === undefined) { if (client === undefined) {
this.roomManagerClients[index] = client = new RoomManagerClient(this.apiUrls[index], grpc.credentials.createInsecure()); this.roomManagerClients[index] = client = new RoomManagerClient(
debug('Mapping room %s to API server %s', roomId, this.apiUrls[index]) this.apiUrls[index],
grpc.credentials.createInsecure()
);
debug("Mapping room %s to API server %s", roomId, this.apiUrls[index]);
} }
return Promise.resolve(client); return Promise.resolve(client);
} }
public async getAllClients(): Promise<RoomManagerClient[]> { public async getAllClients(): Promise<RoomManagerClient[]> {
return [await this.getClient('')]; return [await this.getClient("")];
} }
} }
const apiClientRepository = new ApiClientRepository(API_URL.split(',')); const apiClientRepository = new ApiClientRepository(API_URL.split(","));
export { apiClientRepository }; export { apiClientRepository };

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 {
@ -11,7 +11,7 @@ class ClientEventsEmitter extends EventEmitter {
emitClientLeave(clientUUid: string, roomId: string): void { emitClientLeave(clientUUid: string, roomId: string): void {
this.emit(clientLeaveEvent, clientUUid, roomId); this.emit(clientLeaveEvent, clientUUid, roomId);
} }
registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void { registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void {
this.on(clientJoinEvent, callback); this.on(clientJoinEvent, callback);
} }
@ -29,4 +29,4 @@ class ClientEventsEmitter extends EventEmitter {
} }
} }
export const clientEventsEmitter = new ClientEventsEmitter(); export const clientEventsEmitter = new ClientEventsEmitter();

View File

@ -1,6 +1,6 @@
import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
function secNSec2ms(secNSec: Array<number>|number) { function secNSec2ms(secNSec: Array<number> | number) {
if (Array.isArray(secNSec)) { if (Array.isArray(secNSec)) {
return secNSec[0] * 1000 + secNSec[1] / 1000000; return secNSec[0] * 1000 + secNSec[1] / 1000000;
} }
@ -12,17 +12,17 @@ class CpuTracker {
private overHeating: boolean = false; private overHeating: boolean = false;
constructor() { constructor() {
let time = process.hrtime.bigint() let time = process.hrtime.bigint();
let usage = process.cpuUsage() let usage = process.cpuUsage();
setInterval(() => { setInterval(() => {
const elapTime = process.hrtime.bigint(); const elapTime = process.hrtime.bigint();
const elapUsage = process.cpuUsage(usage) const elapUsage = process.cpuUsage(usage);
usage = process.cpuUsage() usage = process.cpuUsage();
const elapTimeMS = elapTime - time; const elapTimeMS = elapTime - time;
const elapUserMS = secNSec2ms(elapUsage.user) const elapUserMS = secNSec2ms(elapUsage.user);
const elapSystMS = secNSec2ms(elapUsage.system) const elapSystMS = secNSec2ms(elapUsage.system);
this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
time = elapTime; time = elapTime;

View File

@ -1,4 +1,4 @@
import {Counter, Gauge} from "prom-client"; import { Counter, Gauge } from "prom-client";
//this class should manage all the custom metrics used by prometheus //this class should manage all the custom metrics used by prometheus
class GaugeManager { class GaugeManager {
@ -9,25 +9,25 @@ class GaugeManager {
constructor() { constructor() {
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"],
}); });
} }
@ -42,13 +42,13 @@ class GaugeManager {
} }
incNbGroupsPerRoomGauge(roomId: string): void { incNbGroupsPerRoomGauge(roomId: string): void {
this.nbGroupsPerRoomCounter.inc({ room: roomId }) this.nbGroupsPerRoomCounter.inc({ room: roomId });
this.nbGroupsPerRoomGauge.inc({ room: roomId }) this.nbGroupsPerRoomGauge.inc({ room: roomId });
} }
decNbGroupsPerRoomGauge(roomId: string): void { decNbGroupsPerRoomGauge(roomId: string): void {
this.nbGroupsPerRoomGauge.dec({ room: roomId }) this.nbGroupsPerRoomGauge.dec({ room: roomId });
} }
} }
export const gaugeManager = new GaugeManager(); export const gaugeManager = new GaugeManager();

View File

@ -1,6 +1,6 @@
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
import {BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; import { BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage } from "../Messages/generated/messages_pb";
import {WebSocket} from "uWebSockets.js"; import { WebSocket } from "uWebSockets.js";
export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void {
socket.batchedMessages.addPayload(payload); socket.batchedMessages.addPayload(payload);
@ -33,4 +33,3 @@ export function emitError(Client: WebSocket, message: string): void {
} }
console.warn(message); console.warn(message);
} }

View File

@ -1,73 +1,77 @@
import {ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
import {uuid} from "uuidv4"; import { uuid } from "uuidv4";
import Jwt from "jsonwebtoken"; import Jwt from "jsonwebtoken";
import {TokenInterface} from "../Controller/AuthenticateController"; import { TokenInterface } from "../Controller/AuthenticateController";
import {adminApi, AdminBannedData} from "../Services/AdminApi"; import { adminApi, AdminBannedData } from "../Services/AdminApi";
class JWTTokenManager { class JWTTokenManager {
public createJWTToken(userUuid: string) { public createJWTToken(userUuid: string) {
return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token
} }
public async getUserUuidFromToken(token: unknown, ipAddress?: string, room?: string): Promise<string> { public async getUserUuidFromToken(token: unknown, ipAddress?: string, room?: string): Promise<string> {
if (!token) { if (!token) {
throw new Error('An authentication error happened, a user tried to connect without a token.'); throw new Error("An authentication error happened, a user tried to connect without a token.");
} }
if (typeof(token) !== "string") { if (typeof token !== "string") {
throw new Error('Token is expected to be a string'); throw new Error("Token is expected to be a string");
} }
if (token === "test") {
if(token === 'test') {
if (ALLOW_ARTILLERY) { if (ALLOW_ARTILLERY) {
return uuid(); return uuid();
} else { } else {
throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); throw new Error(
"In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"
);
} }
} }
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { Jwt.verify(token, SECRET_KEY, {}, (err, tokenDecoded) => {
const tokenInterface = tokenDecoded as TokenInterface; const tokenInterface = tokenDecoded as TokenInterface;
if (err) { if (err) {
console.error('An authentication error happened, invalid JsonWebToken.', err); console.error("An authentication error happened, invalid JsonWebToken.", err);
reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message)); reject(new Error("An authentication error happened, invalid JsonWebToken. " + err.message));
return; return;
} }
if (tokenDecoded === undefined) { if (tokenDecoded === undefined) {
console.error('Empty token found.'); console.error("Empty token found.");
reject(new Error('Empty token found.')); reject(new Error("Empty token found."));
return; return;
} }
//verify token //verify token
if (!this.isValidToken(tokenInterface)) { if (!this.isValidToken(tokenInterface)) {
reject(new Error('Authentication error, invalid token structure.')); reject(new Error("Authentication error, invalid token structure."));
return; return;
} }
if (ADMIN_API_URL) { if (ADMIN_API_URL) {
//verify user in admin //verify user in admin
let promise = new Promise((resolve) => resolve()); let promise = new Promise((resolve) => resolve());
if(ipAddress && room) { if (ipAddress && room) {
promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, room); promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, room);
} }
promise.then(() => { promise
adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => { .then(() => {
resolve(tokenInterface.userUuid); adminApi
}).catch((err) => { .fetchCheckUserByToken(tokenInterface.userUuid)
//anonymous user .then(() => {
if (err.response && err.response.status && err.response.status === 404) { resolve(tokenInterface.userUuid);
resolve(tokenInterface.userUuid); })
return; .catch((err) => {
} //anonymous user
if (err.response && err.response.status && err.response.status === 404) {
resolve(tokenInterface.userUuid);
return;
}
reject(err);
});
})
.catch((err) => {
reject(err); reject(err);
}); });
}).catch((err) => {
reject(err);
});
} else { } else {
resolve(tokenInterface.userUuid); resolve(tokenInterface.userUuid);
} }
@ -76,30 +80,32 @@ class JWTTokenManager {
} }
private verifyBanUser(userUuid: string, ipAddress: string, room: string): Promise<AdminBannedData> { private verifyBanUser(userUuid: string, ipAddress: string, room: string): Promise<AdminBannedData> {
const parts = room.split('/'); const parts = room.split("/");
if (parts.length < 3 || parts[0] !== '@') { if (parts.length < 3 || parts[0] !== "@") {
return Promise.resolve({ return Promise.resolve({
is_banned: false, is_banned: false,
message: '' message: "",
}); });
} }
const organization = parts[1]; const organization = parts[1];
const world = parts[2]; const world = parts[2];
return adminApi.verifyBanUser(userUuid, ipAddress, organization, world).then((data: AdminBannedData) => { return adminApi
if (data && data.is_banned) { .verifyBanUser(userUuid, ipAddress, organization, world)
throw new Error('User was banned'); .then((data: AdminBannedData) => {
} if (data && data.is_banned) {
return data; throw new Error("User was banned");
}).catch((err) => { }
throw err; return data;
}); })
.catch((err) => {
throw err;
});
} }
private isValidToken(token: object): token is TokenInterface { private isValidToken(token: object): token is TokenInterface {
return !(typeof((token as TokenInterface).userUuid) !== 'string'); return !(typeof (token as TokenInterface).userUuid !== "string");
} }
} }
export const jwtTokenManager = new JWTTokenManager(); export const jwtTokenManager = new JWTTokenManager();

View File

@ -1,5 +1,5 @@
import {PusherRoom} from "../Model/PusherRoom"; import { PusherRoom } from "../Model/PusherRoom";
import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
import { import {
GroupDeleteMessage, GroupDeleteMessage,
ItemEventMessage, ItemEventMessage,
@ -31,21 +31,21 @@ import {
RefreshRoomMessage, RefreshRoomMessage,
EmotePromptMessage, EmotePromptMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; import { JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
import {adminApi, CharacterTexture} from "./AdminApi"; import { adminApi, CharacterTexture } from "./AdminApi";
import {emitInBatch} from "./IoSocketHelpers"; import { emitInBatch } from "./IoSocketHelpers";
import Jwt from "jsonwebtoken"; import Jwt from "jsonwebtoken";
import {JITSI_URL} from "../Enum/EnvironmentVariable"; import { JITSI_URL } from "../Enum/EnvironmentVariable";
import {clientEventsEmitter} from "./ClientEventsEmitter"; import { clientEventsEmitter } from "./ClientEventsEmitter";
import {gaugeManager} from "./GaugeManager"; import { gaugeManager } from "./GaugeManager";
import {apiClientRepository} from "./ApiClientRepository"; import { apiClientRepository } from "./ApiClientRepository";
import {GroupDescriptor, UserDescriptor, ZoneEventListener} from "_Model/Zone"; import { GroupDescriptor, UserDescriptor, ZoneEventListener } from "_Model/Zone";
import Debug from "debug"; import Debug from "debug";
import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
import {WebSocket} from "uWebSockets.js"; import { WebSocket } from "uWebSockets.js";
const debug = Debug('socket'); const debug = Debug("socket");
interface AdminSocketRoomsList { interface AdminSocketRoomsList {
[index: string]: number; [index: string]: number;
@ -55,12 +55,11 @@ interface AdminSocketUsersList {
} }
export interface AdminSocketData { export interface AdminSocketData {
rooms: AdminSocketRoomsList, rooms: AdminSocketRoomsList;
users: AdminSocketUsersList, users: AdminSocketUsersList;
} }
export class SocketManager implements ZoneEventListener { export class SocketManager implements ZoneEventListener {
private rooms: Map<string, PusherRoom> = new Map<string, PusherRoom>(); private rooms: Map<string, PusherRoom> = new Map<string, PusherRoom>();
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>(); private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
@ -78,47 +77,53 @@ export class SocketManager implements ZoneEventListener {
const adminRoomStream = apiClient.adminRoom(); const adminRoomStream = apiClient.adminRoom();
client.adminConnection = adminRoomStream; client.adminConnection = adminRoomStream;
adminRoomStream.on('data', (message: ServerToAdminClientMessage) => { adminRoomStream
.on("data", (message: ServerToAdminClientMessage) => {
if (message.hasUserjoinedroom()) { if (message.hasUserjoinedroom()) {
const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage;
if (!client.disconnecting) { if (!client.disconnecting) {
client.send(JSON.stringify({ client.send(
type: 'MemberJoin', JSON.stringify({
data: { type: "MemberJoin",
uuid: userJoinedRoomMessage.getUuid(), data: {
name: userJoinedRoomMessage.getName(), uuid: userJoinedRoomMessage.getUuid(),
ipAddress: userJoinedRoomMessage.getIpaddress(), name: userJoinedRoomMessage.getName(),
roomId: roomId, ipAddress: userJoinedRoomMessage.getIpaddress(),
} roomId: roomId,
})); },
})
);
}
} else if (message.hasUserleftroom()) {
const userLeftRoomMessage = message.getUserleftroom() as UserLeftRoomMessage;
if (!client.disconnecting) {
client.send(
JSON.stringify({
type: "MemberLeave",
data: {
uuid: userLeftRoomMessage.getUuid(),
},
})
);
}
} else {
throw new Error("Unexpected admin message");
} }
} else if (message.hasUserleftroom()) { })
const userLeftRoomMessage = message.getUserleftroom() as UserLeftRoomMessage; .on("end", () => {
console.warn("Admin connection lost to back server");
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start.
if (!client.disconnecting) { if (!client.disconnecting) {
client.send(JSON.stringify({ this.closeWebsocketConnection(client, 1011, "Connection lost to back server");
type: 'MemberLeave',
data: {
uuid: userLeftRoomMessage.getUuid()
}
}));
} }
} else { console.log("A user left");
throw new Error('Unexpected admin message'); })
} .on("error", (err: Error) => {
}).on('end', () => { console.error("Error in connection to back server:", err);
console.warn('Admin connection lost to back server'); if (!client.disconnecting) {
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. this.closeWebsocketConnection(client, 1011, "Error while connecting to back server");
if (!client.disconnecting) { }
this.closeWebsocketConnection(client, 1011, 'Connection lost to back server'); });
}
console.log('A user left');
}).on('error', (err: Error) => {
console.error('Error in connection to back server:', err);
if (!client.disconnecting) {
this.closeWebsocketConnection(client, 1011, 'Error while connecting to back server');
}
});
const message = new AdminPusherToBackMessage(); const message = new AdminPusherToBackMessage();
message.setSubscribetoroom(roomId); message.setSubscribetoroom(roomId);
@ -126,14 +131,14 @@ export class SocketManager implements ZoneEventListener {
adminRoomStream.write(message); adminRoomStream.write(message);
} }
leaveAdminRoom(socket : ExAdminSocketInterface) { leaveAdminRoom(socket: ExAdminSocketInterface) {
if (socket.adminConnection) { if (socket.adminConnection) {
socket.adminConnection.end(); socket.adminConnection.end();
} }
} }
getAdminSocketDataFor(roomId:string): AdminSocketData { getAdminSocketDataFor(roomId: string): AdminSocketData {
throw new Error('Not reimplemented yet'); throw new Error("Not reimplemented yet");
/*const data:AdminSocketData = { /*const data:AdminSocketData = {
rooms: {}, rooms: {},
users: {}, users: {},
@ -153,7 +158,6 @@ export class SocketManager implements ZoneEventListener {
async handleJoinRoom(client: ExSocketInterface): Promise<void> { async handleJoinRoom(client: ExSocketInterface): Promise<void> {
const viewport = client.viewport; const viewport = client.viewport;
try { try {
const joinRoomMessage = new JoinRoomMessage(); const joinRoomMessage = new JoinRoomMessage();
joinRoomMessage.setUseruuid(client.userUuid); joinRoomMessage.setUseruuid(client.userUuid);
joinRoomMessage.setIpaddress(client.IPAddress); joinRoomMessage.setIpaddress(client.IPAddress);
@ -176,46 +180,49 @@ export class SocketManager implements ZoneEventListener {
joinRoomMessage.addCharacterlayer(characterLayerMessage); joinRoomMessage.addCharacterlayer(characterLayerMessage);
} }
console.log("Calling joinRoom");
console.log('Calling joinRoom')
const apiClient = await apiClientRepository.getClient(client.roomId); const apiClient = await apiClientRepository.getClient(client.roomId);
const streamToPusher = apiClient.joinRoom(); const streamToPusher = apiClient.joinRoom();
clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId);
client.backConnection = streamToPusher; client.backConnection = streamToPusher;
streamToPusher.on('data', (message: ServerToClientMessage) => { streamToPusher
if (message.hasRoomjoinedmessage()) { .on("data", (message: ServerToClientMessage) => {
client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); if (message.hasRoomjoinedmessage()) {
// TODO: do we need this.sockets anymore? client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid();
this.sockets.set(client.userId, client); // TODO: do we need this.sockets anymore?
this.sockets.set(client.userId, client);
// If this is the first message sent, send back the viewport. // If this is the first message sent, send back the viewport.
this.handleViewport(client, viewport); this.handleViewport(client, viewport);
} }
if (message.hasRefreshroommessage()) {
const refreshMessage:RefreshRoomMessage = message.getRefreshroommessage() as unknown as RefreshRoomMessage;
this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber())
}
// Let's pass data over from the back to the client. if (message.hasRefreshroommessage()) {
if (!client.disconnecting) { const refreshMessage: RefreshRoomMessage =
client.send(message.serializeBinary().buffer, true); message.getRefreshroommessage() as unknown as RefreshRoomMessage;
} this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber());
}).on('end', () => { }
console.warn('Connection lost to back server');
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. // Let's pass data over from the back to the client.
if (!client.disconnecting) { if (!client.disconnecting) {
this.closeWebsocketConnection(client, 1011, 'Connection lost to back server'); client.send(message.serializeBinary().buffer, true);
} }
console.log('A user left'); })
}).on('error', (err: Error) => { .on("end", () => {
console.error('Error in connection to back server:', err); console.warn("Connection lost to back server");
if (!client.disconnecting) { // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start.
this.closeWebsocketConnection(client, 1011, 'Error while connecting to back server'); if (!client.disconnecting) {
} this.closeWebsocketConnection(client, 1011, "Connection lost to back server");
}); }
console.log("A user left");
})
.on("error", (err: Error) => {
console.error("Error in connection to back server:", err);
if (!client.disconnecting) {
this.closeWebsocketConnection(client, 1011, "Error while connecting to back server");
}
});
const pusherToBackMessage = new PusherToBackMessage(); const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setJoinroommessage(joinRoomMessage); pusherToBackMessage.setJoinroommessage(joinRoomMessage);
@ -226,7 +233,7 @@ export class SocketManager implements ZoneEventListener {
} }
} }
private closeWebsocketConnection(client: ExSocketInterface|ExAdminSocketInterface, code: number, reason: string) { private closeWebsocketConnection(client: ExSocketInterface | ExAdminSocketInterface, code: number, reason: string) {
client.disconnecting = true; client.disconnecting = true;
//this.leaveRoom(client); //this.leaveRoom(client);
//client.close(); //client.close();
@ -257,15 +264,13 @@ export class SocketManager implements ZoneEventListener {
const viewport = userMovesMessage.getViewport(); const viewport = userMovesMessage.getViewport();
if (viewport === undefined) { if (viewport === undefined) {
throw new Error('Missing viewport in UserMovesMessage'); throw new Error("Missing viewport in UserMovesMessage");
} }
// Now, we need to listen to the correct viewport. // Now, we need to listen to the correct viewport.
this.handleViewport(client, viewport.toObject()) this.handleViewport(client, viewport.toObject());
} }
onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void { onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void {
const subMessage = new SubMessage(); const subMessage = new SubMessage();
subMessage.setEmoteeventmessage(emoteMessage); subMessage.setEmoteeventmessage(emoteMessage);
@ -299,11 +304,16 @@ export class SocketManager implements ZoneEventListener {
try { try {
const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid());
if (!reportedSocket) { if (!reportedSocket) {
throw 'reported socket user not found'; throw "reported socket user not found";
} }
//TODO report user on admin application //TODO report user on admin application
//todo: move to back because this fail if the reported player is in another pusher. //todo: move to back because this fail if the reported player is in another pusher.
await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2]) await adminApi.reportPlayer(
reportedSocket.userUuid,
reportPlayerMessage.getReportcomment(),
client.userUuid,
client.roomId.split("/")[2]
);
} catch (e) { } catch (e) {
console.error('An error occurred on "handleReportMessage"'); console.error('An error occurred on "handleReportMessage"');
console.error(e); console.error(e);
@ -325,14 +335,14 @@ export class SocketManager implements ZoneEventListener {
} }
private searchClientByIdOrFail(userId: number): ExSocketInterface { private searchClientByIdOrFail(userId: number): ExSocketInterface {
const client: ExSocketInterface|undefined = this.sockets.get(userId); const client: ExSocketInterface | undefined = this.sockets.get(userId);
if (client === undefined) { if (client === undefined) {
throw new Error("Could not find user with id " + userId); throw new Error("Could not find user with id " + userId);
} }
return client; return client;
} }
leaveRoom(socket : ExSocketInterface) { leaveRoom(socket: ExSocketInterface) {
// leave previous room and world // leave previous room and world
try { try {
if (socket.roomId) { if (socket.roomId) {
@ -340,15 +350,15 @@ export class SocketManager implements ZoneEventListener {
//user leaves room //user leaves room
const room: PusherRoom | undefined = this.rooms.get(socket.roomId); const room: PusherRoom | undefined = this.rooms.get(socket.roomId);
if (room) { if (room) {
debug('Leaving room %s.', socket.roomId); debug("Leaving room %s.", socket.roomId);
room.leave(socket); room.leave(socket);
if (room.isEmpty()) { if (room.isEmpty()) {
this.rooms.delete(socket.roomId); this.rooms.delete(socket.roomId);
debug('Room %s is empty. Deleting.', socket.roomId); debug("Room %s is empty. Deleting.", socket.roomId);
} }
} else { } else {
console.error('Could not find the GameRoom the user is leaving!'); console.error("Could not find the GameRoom the user is leaving!");
} }
//user leave previous room //user leave previous room
//Client.leave(Client.roomId); //Client.leave(Client.roomId);
@ -356,7 +366,7 @@ export class SocketManager implements ZoneEventListener {
//delete Client.roomId; //delete Client.roomId;
this.sockets.delete(socket.userId); this.sockets.delete(socket.userId);
clientEventsEmitter.emitClientLeave(socket.userUuid, socket.roomId); clientEventsEmitter.emitClientLeave(socket.userUuid, socket.roomId);
console.log('A user left (', this.sockets.size, ' connected users)'); console.log("A user left (", this.sockets.size, " connected users)");
} }
} }
} finally { } finally {
@ -368,27 +378,27 @@ export class SocketManager implements ZoneEventListener {
async getOrCreateRoom(roomId: string): Promise<PusherRoom> { async getOrCreateRoom(roomId: string): Promise<PusherRoom> {
//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 PusherRoom(roomId, this); world = new PusherRoom(roomId, this);
if (!world.public) { if (!world.public) {
await this.updateRoomWithAdminData(world); await this.updateRoomWithAdminData(world);
} }
this.rooms.set(roomId, world); this.rooms.set(roomId, world);
} }
return Promise.resolve(world) return Promise.resolve(world);
} }
public async updateRoomWithAdminData(world: PusherRoom): Promise<void> { public async updateRoomWithAdminData(world: PusherRoom): Promise<void> {
const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug);
world.tags = data.tags; world.tags = data.tags;
world.policyType = Number(data.policy_type); world.policyType = Number(data.policy_type);
} }
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) { emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
if (!client.tags.includes('admin')) { if (!client.tags.includes("admin")) {
//In case of xss injection, we just kill the connection. //In case of xss injection, we just kill the connection.
throw 'Client is not an admin!'; throw "Client is not an admin!";
} }
const pusherToBackMessage = new PusherToBackMessage(); const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setPlayglobalmessage(playglobalmessage); pusherToBackMessage.setPlayglobalmessage(playglobalmessage);
@ -399,44 +409,48 @@ export class SocketManager implements ZoneEventListener {
public getWorlds(): Map<string, PusherRoom> { public getWorlds(): Map<string, PusherRoom> {
return this.rooms; return this.rooms;
} }
searchClientByUuid(uuid: string): ExSocketInterface | null { searchClientByUuid(uuid: string): ExSocketInterface | null {
for(const socket of this.sockets.values()){ for (const socket of this.sockets.values()) {
if(socket.userUuid === uuid){ if (socket.userUuid === uuid) {
return socket; return socket;
} }
} }
return null; return null;
} }
public handleQueryJitsiJwtMessage(client: ExSocketInterface, queryJitsiJwtMessage: QueryJitsiJwtMessage) { public handleQueryJitsiJwtMessage(client: ExSocketInterface, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
try { try {
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 = client.tags.includes(tag); const isAdmin = client.tags.includes(tag);
const jwt = Jwt.sign({ const jwt = Jwt.sign(
"aud": "jitsi", {
"iss": JITSI_ISS, aud: "jitsi",
"sub": JITSI_URL, iss: JITSI_ISS,
"room": room, sub: JITSI_URL,
"moderator": isAdmin room: room,
}, SECRET_JITSI_KEY, { moderator: isAdmin,
expiresIn: '1d', },
algorithm: "HS256", SECRET_JITSI_KEY,
header: {
{ expiresIn: "1d",
"alg": "HS256", algorithm: "HS256",
"typ": "JWT" header: {
} alg: "HS256",
}); typ: "JWT",
},
}
);
const sendJitsiJwtMessage = new SendJitsiJwtMessage(); const sendJitsiJwtMessage = new SendJitsiJwtMessage();
sendJitsiJwtMessage.setJitsiroom(room); sendJitsiJwtMessage.setJitsiroom(room);
@ -447,7 +461,7 @@ export class SocketManager implements ZoneEventListener {
client.send(serverToClientMessage.serializeBinary().buffer, true); client.send(serverToClientMessage.serializeBinary().buffer, true);
} catch (e) { } catch (e) {
console.error('An error occured while generating the Jitsi JWT token: ', e); console.error("An error occured while generating the Jitsi JWT token: ", e);
} }
} }
@ -471,7 +485,7 @@ export class SocketManager implements ZoneEventListener {
backAdminMessage.setType(type); backAdminMessage.setType(type);
backConnection.sendAdminMessage(backAdminMessage, (error) => { backConnection.sendAdminMessage(backAdminMessage, (error) => {
if (error !== null) { if (error !== null) {
console.error('Error while sending admin message', error); console.error("Error while sending admin message", error);
} }
}); });
} }
@ -496,7 +510,7 @@ export class SocketManager implements ZoneEventListener {
banMessage.setType(type); banMessage.setType(type);
backConnection.ban(banMessage, (error) => { backConnection.ban(banMessage, (error) => {
if (error !== null) { if (error !== null) {
console.error('Error while sending admin message', error); console.error("Error while sending admin message", error);
} }
}); });
} }
@ -504,25 +518,28 @@ export class SocketManager implements ZoneEventListener {
/** /**
* Merges the characterLayers received from the front (as an array of string) with the custom textures from the back. * Merges the characterLayers received from the front (as an array of string) with the custom textures from the back.
*/ */
static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] { static mergeCharacterLayersAndCustomTextures(
characterLayers: string[],
memberTextures: CharacterTexture[]
): CharacterLayer[] {
const characterLayerObjs: CharacterLayer[] = []; const characterLayerObjs: CharacterLayer[] = [];
for (const characterLayer of characterLayers) { for (const characterLayer of characterLayers) {
if (characterLayer.startsWith('customCharacterTexture')) { if (characterLayer.startsWith("customCharacterTexture")) {
const customCharacterLayerId: number = +characterLayer.substr(22); const customCharacterLayerId: number = +characterLayer.substr(22);
for (const memberTexture of memberTextures) { for (const memberTexture of memberTextures) {
if (memberTexture.id == customCharacterLayerId) { if (memberTexture.id == customCharacterLayerId) {
characterLayerObjs.push({ characterLayerObjs.push({
name: characterLayer, name: characterLayer,
url: memberTexture.url url: memberTexture.url,
}) });
break; break;
} }
} }
} else { } else {
characterLayerObjs.push({ characterLayerObjs.push({
name: characterLayer, name: characterLayer,
url: undefined url: undefined,
}) });
} }
} }
return characterLayerObjs; return characterLayerObjs;
@ -572,7 +589,7 @@ export class SocketManager implements ZoneEventListener {
emitInBatch(listener, subMessage); emitInBatch(listener, subMessage);
} }
public emitWorldFullMessage(client: WebSocket) { public emitWorldFullMessage(client: WebSocket) {
const errorMessage = new WorldFullMessage(); const errorMessage = new WorldFullMessage();
@ -594,9 +611,9 @@ export class SocketManager implements ZoneEventListener {
private refreshRoomData(roomId: string, versionNumber: number): void { private refreshRoomData(roomId: string, versionNumber: number): void {
const room = this.rooms.get(roomId); const room = this.rooms.get(roomId);
//this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed. //this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed.
if (!room || !room.needsUpdate(versionNumber)) return; if (!room || !room.needsUpdate(versionNumber)) return;
this.updateRoomWithAdminData(room); this.updateRoomWithAdminData(room);
} }