diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..3a2a562d --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ develop ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] + schedule: + - cron: '24 17 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 2036e4e6..faf50c7a 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -64,6 +64,11 @@ jobs: run: yarn test 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: name: "Continuous Integration Pusher" @@ -107,6 +112,10 @@ jobs: run: yarn test working-directory: "pusher" + - name: "Prettier" + run: yarn run pretty-check + working-directory: "pusher" + continuous-integration-back: name: "Continuous Integration Back" @@ -150,3 +159,7 @@ jobs: run: yarn test working-directory: "back" + - name: "Prettier" + run: yarn run pretty-check + working-directory: "back" + diff --git a/.gitignore b/.gitignore index 70660058..2cbf66b1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ docker-compose.override.yaml *.DS_Store maps/yarn.lock maps/dist/computer.js -maps/dist/computer.js.map \ No newline at end of file +maps/dist/computer.js.map +/node_modules/ diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 00000000..31354ec1 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..5944be37 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,15 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +( +cd front || exit +yarn run precommit +) +( +cd pusher || exit +yarn run precommit +) +( +cd back || exit +yarn run precommit +) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b85d0a98 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing to WorkAdventure + +Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to +ask questions and how to work on something. + +## Contributions we are seeking + +We love to receive contributions from our community — you! + +There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, +submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself. + +## Using the issue tracker + +First things first: **Do NOT report security vulnerabilities in public issues!**. +Please read the [security guide](SECURITY.md) to learn who to do a security disclosure to the WorkAdventure core team. + +You can use [GitHub issue tracker](https://github.com/thecodingmachine/workadventure/issues) to: + +- File bug reports +- Ask for feature requests + +If you have more general questions, a good place to ask is [our Discord server](https://discord.gg/YGtngdh9gt). + +Finally, you can come and talk to the WorkAdventure core team... on WorkAdventure, of course! [Our offices are here](https://play.staging.workadventu.re/@/tcm/workadventure/wa-village). + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope +and avoid containing unrelated commits. + +Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), +otherwise you risk spending a lot of time working on something that the project's developers might not want to merge +into the project. + +You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues). + +### Linting your code + +Before committing, be sure to install the "Prettier" precommit hook that will reformat your code to our coding style. + +In order to enable the "Prettier" precommit hook, at the root of the project, run: + +```console +$ yarn run install +$ 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 + +WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test. + +Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine). + +If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain +some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file +to add a reference to your newly created test map. + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..562da3e0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Reporting a Vulnerability + +First things first: **Do NOT report security vulnerabilities in public issues!** + +Please disclose responsibly by sending +a mail at security@workadventu.re (you can also ping us in the GitHub issues, but please, no details in the issues!) + +We will assess the issue as soon as possible on a best-effort basis and will give you an estimate for when we have a fix +and release available for an eventual public disclosure. + +We do not have a bug bounty program. + +## Supported Versions + +We only apply security patches on the latest tagged release and on the `master` and `develop` branches + +Unless specified otherwise, do not expect us to fix security issues on past releases. We are only maintaining one release: +the latest one, which is online at https://play.workadventu.re. diff --git a/back/.prettierignore b/back/.prettierignore new file mode 100644 index 00000000..1f453464 --- /dev/null +++ b/back/.prettierignore @@ -0,0 +1 @@ +src/Messages/generated diff --git a/back/.prettierrc.json b/back/.prettierrc.json new file mode 100644 index 00000000..e8980d15 --- /dev/null +++ b/back/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "tabWidth": 4 +} diff --git a/back/package.json b/back/package.json index a6ff7319..7015b9b8 100644 --- a/back/package.json +++ b/back/package.json @@ -10,8 +10,11 @@ "runprod": "node --max-old-space-size=4096 ./dist/server.js", "profile": "tsc && node --prof ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", - "lint": "node_modules/.bin/eslint src/ . --ext .ts", - "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts" + "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", + "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", + "precommit": "lint-staged", + "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", + "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" }, "repository": { "type": "git", @@ -66,7 +69,14 @@ "@typescript-eslint/parser": "^2.26.0", "eslint": "^6.8.0", "jasmine": "^3.5.0", + "lint-staged": "^11.0.0", + "prettier": "^2.3.1", "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3" + }, + "lint-staged": { + "*.ts": [ + "prettier --write" + ] } } diff --git a/back/src/App.ts b/back/src/App.ts index 4bcc56ba..a6c42abb 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -1,7 +1,7 @@ // lib/app.ts -import {PrometheusController} from "./Controller/PrometheusController"; -import {DebugController} from "./Controller/DebugController"; -import {App as uwsApp} from "./Server/sifrr.server"; +import { PrometheusController } from "./Controller/PrometheusController"; +import { DebugController } from "./Controller/DebugController"; +import { App as uwsApp } from "./Server/sifrr.server"; class App { public app: uwsApp; diff --git a/back/src/Controller/BaseController.ts b/back/src/Controller/BaseController.ts index 93c17ab4..dc510d6c 100644 --- a/back/src/Controller/BaseController.ts +++ b/back/src/Controller/BaseController.ts @@ -1,10 +1,9 @@ -import {HttpResponse} from "uWebSockets.js"; - +import { HttpResponse } from "uWebSockets.js"; export class BaseController { protected addCorsHeaders(res: HttpResponse): void { - 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-origin', '*'); + 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-origin", "*"); } } diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index 509d8b2f..b7f037fd 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -1,53 +1,54 @@ -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {stringify} from "circular-json"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -import { parse } from 'query-string'; -import {App} from "../Server/sifrr.server"; -import {socketManager} from "../Services/SocketManager"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { stringify } from "circular-json"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +import { parse } from "query-string"; +import { App } from "../Server/sifrr.server"; +import { socketManager } from "../Services/SocketManager"; export class DebugController { - constructor(private App : App) { + constructor(private App: App) { this.getDump(); } - - getDump(){ + getDump() { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { const query = parse(req.getQuery()); 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( - socketManager.getWorlds(), - (key: unknown, value: unknown) => { - if (key === 'listeners') { - return 'Listeners'; - } - if (key === 'socket') { - 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 res + .writeStatus("200 OK") + .writeHeader("Content-Type", "application/json") + .end( + stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { + if (key === "listeners") { + return "Listeners"; } - return obj; - } else if(value instanceof Set) { + if (key === "socket") { + 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 = []; for (const [setKey, setValue] of value.entries()) { obj.push(setValue); } return obj; - } else { - return value; - } - } - )); + } else { + return value; + } + }) + ); }); } } diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts index e854cf43..3ab3d33f 100644 --- a/back/src/Controller/PrometheusController.ts +++ b/back/src/Controller/PrometheusController.ts @@ -1,7 +1,7 @@ -import {App} from "../Server/sifrr.server"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -const register = require('prom-client').register; -const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; +import { App } from "../Server/sifrr.server"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +const register = require("prom-client").register; +const collectDefaultMetrics = require("prom-client").collectDefaultMetrics; export class PrometheusController { constructor(private App: App) { @@ -14,7 +14,7 @@ export class PrometheusController { } private metrics(res: HttpResponse, req: HttpRequest): void { - res.writeHeader('Content-Type', register.contentType); + res.writeHeader("Content-Type", register.contentType); res.end(register.metrics()); } } diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 81693a98..19eddd3e 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -1,17 +1,17 @@ const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; -const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; -const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; +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_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; 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_ISS = process.env.JITSI_ISS || ''; -const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; -const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080; -const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051; +const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; +const JITSI_ISS = process.env.JITSI_ISS || ""; +const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; +const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080; +const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051; export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed -export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ''; -export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4'); +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 { MINIMUM_DISTANCE, @@ -24,5 +24,5 @@ export { CPU_OVERHEAT_THRESHOLD, JITSI_URL, JITSI_ISS, - SECRET_JITSI_KEY -} + SECRET_JITSI_KEY, +}; diff --git a/back/src/Model/Admin.ts b/back/src/Model/Admin.ts index 29b53385..93396fa8 100644 --- a/back/src/Model/Admin.ts +++ b/back/src/Model/Admin.ts @@ -1,15 +1,12 @@ import { ServerToAdminClientMessage, - UserJoinedRoomMessage, UserLeftRoomMessage + UserJoinedRoomMessage, + UserLeftRoomMessage, } from "../Messages/generated/messages_pb"; -import {AdminSocket} from "../RoomManager"; - +import { AdminSocket } from "../RoomManager"; export class Admin { - public constructor( - private readonly socket: AdminSocket - ) { - } + public constructor(private readonly socket: AdminSocket) {} public sendUserJoin(uuid: string, name: string, ip: string): void { const serverToAdminClientMessage = new ServerToAdminClientMessage(); @@ -24,7 +21,7 @@ export class Admin { 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 userLeftRoomMessage = new UserLeftRoomMessage(); diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 53d0a855..020f4c29 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -1,16 +1,16 @@ -import {PointInterface} from "./Websocket/PointInterface"; -import {Group} from "./Group"; -import {User, UserSocket} from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; -import {PositionNotifier} from "./PositionNotifier"; -import {Movable} from "_Model/Movable"; -import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; -import {arrayIntersect} from "../Services/ArrayHelper"; -import {EmoteEventMessage, JoinRoomMessage} from "../Messages/generated/messages_pb"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {ZoneSocket} from "src/RoomManager"; -import {Admin} from "../Model/Admin"; +import { PointInterface } from "./Websocket/PointInterface"; +import { Group } from "./Group"; +import { User, UserSocket } from "./User"; +import { PositionInterface } from "_Model/PositionInterface"; +import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone"; +import { PositionNotifier } from "./PositionNotifier"; +import { Movable } from "_Model/Movable"; +import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; +import { arrayIntersect } from "../Services/ArrayHelper"; +import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { ZoneSocket } from "src/RoomManager"; +import { Admin } from "../Model/Admin"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; @@ -39,33 +39,33 @@ export class GameRoom { private readonly positionNotifier: PositionNotifier; public readonly roomId: string; public readonly roomSlug: string; - public readonly worldSlug: string = ''; - public readonly organizationSlug: string = ''; - private versionNumber:number = 1; + public readonly worldSlug: string = ""; + public readonly organizationSlug: string = ""; + private versionNumber: number = 1; private nextUserId: number = 1; - constructor(roomId: string, - connectCallback: ConnectCallback, - disconnectCallback: DisconnectCallback, - minDistance: number, - groupRadius: number, - onEnters: EntersCallback, - onMoves: MovesCallback, - onLeaves: LeavesCallback, - onEmote: EmoteCallback, + constructor( + roomId: string, + connectCallback: ConnectCallback, + disconnectCallback: DisconnectCallback, + minDistance: number, + groupRadius: number, + onEnters: EntersCallback, + onMoves: MovesCallback, + onLeaves: LeavesCallback, + onEmote: EmoteCallback ) { this.roomId = roomId; if (isRoomAnonymous(roomId)) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { - const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId); this.roomSlug = roomSlug; this.organizationSlug = organizationSlug; this.worldSlug = worldSlug; } - this.users = new Map(); this.usersByUuid = new Map(); this.admins = new Set(); @@ -86,21 +86,22 @@ export class GameRoom { return this.users; } - public getUserByUuid(uuid: string): User|undefined { + public getUserByUuid(uuid: string): User | undefined { return this.usersByUuid.get(uuid); } - public getUserById(id: number): User|undefined { + public getUserById(id: number): User | undefined { return this.users.get(id); } - - public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User { + + public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User { const positionMessage = joinRoomMessage.getPositionmessage(); if (positionMessage === undefined) { - throw new Error('Missing position message'); + throw new Error("Missing position message"); } const position = ProtobufUtils.toPointInterface(positionMessage); - const user = new User(this.nextUserId, + const user = new User( + this.nextUserId, joinRoomMessage.getUseruuid(), joinRoomMessage.getIpaddress(), position, @@ -126,12 +127,12 @@ export class GameRoom { return user; } - public leave(user : User){ + public leave(user: User) { const userObj = this.users.get(user.id); 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.users.delete(user.id); @@ -143,7 +144,7 @@ export class GameRoom { // Notify 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; } - public updatePosition(user : User, userPosition: PointInterface): void { + public updatePosition(user: User, userPosition: PointInterface): void { user.setPosition(userPosition); this.updateUserGroup(user); @@ -173,22 +174,24 @@ export class GameRoom { return; } - const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user); + const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user); if (closestItem !== null) { if (closestItem instanceof Group) { // Let's join the group! closestItem.join(user); } else { - const closestUser : User = closestItem; - const group: Group = new Group(this.roomId,[ - user, - closestUser - ], this.connectCallback, this.disconnectCallback, this.positionNotifier); + const closestUser: User = closestItem; + const group: Group = new Group( + this.roomId, + [user, closestUser], + this.connectCallback, + this.disconnectCallback, + this.positionNotifier + ); this.groups.add(group); } } - } else { // If the user is part of a group: // should he leave the group? @@ -229,7 +232,9 @@ export class GameRoom { this.positionNotifier.leave(group); group.destroy(); 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); //todo: is the group garbage collected? @@ -247,16 +252,15 @@ export class GameRoom { * OR * - 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 matchingItem: User | Group | null = null; this.users.forEach((currentUser, userId) => { // Let's only check users that are not part of a group - if (typeof currentUser.group !== 'undefined') { + if (typeof currentUser.group !== "undefined") { return; } - if(currentUser === user) { + if (currentUser === user) { return; } if (currentUser.silent) { @@ -265,7 +269,7 @@ export class GameRoom { const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. - if(distance <= minimumDistanceFound && distance <= this.minDistance) { + if (distance <= minimumDistanceFound && distance <= this.minDistance) { minimumDistanceFound = distance; matchingItem = currentUser; } @@ -276,7 +280,7 @@ export class GameRoom { return; } const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); - if(distance <= minimumDistanceFound && distance <= this.groupRadius) { + if (distance <= minimumDistanceFound && distance <= this.groupRadius) { minimumDistanceFound = distance; matchingItem = group; } @@ -285,15 +289,15 @@ export class GameRoom { return matchingItem; } - public static computeDistance(user1: User, user2: User): number - { + public static computeDistance(user1: User, user2: User): number { const user1Position = user1.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)); } @@ -325,9 +329,9 @@ export class GameRoom { public adminLeave(admin: Admin): void { this.admins.delete(admin); } - + public incrementVersion(): number { - this.versionNumber++ + this.versionNumber++; return this.versionNumber; } diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index ffe7a78a..5a0f3be6 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,13 +1,12 @@ import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { User } from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {gaugeManager} from "../Services/GaugeManager"; -import {MAX_PER_GROUP} from "../Enum/EnvironmentVariable"; +import { PositionInterface } from "_Model/PositionInterface"; +import { Movable } from "_Model/Movable"; +import { PositionNotifier } from "_Model/PositionNotifier"; +import { gaugeManager } from "../Services/GaugeManager"; +import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable"; export class Group implements Movable { - private static nextId: number = 1; private id: number; @@ -18,8 +17,13 @@ export class Group implements Movable { private wasDestroyed: boolean = false; private roomId: string; - - constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { + constructor( + roomId: string, + users: User[], + private connectCallback: ConnectCallback, + private disconnectCallback: DisconnectCallback, + private positionNotifier: PositionNotifier + ) { this.roomId = roomId; this.users = new Set(); this.id = Group.nextId; @@ -43,7 +47,7 @@ export class Group implements Movable { return Array.from(this.users.values()); } - getId() : number { + getId(): number { return this.id; } @@ -53,7 +57,7 @@ export class Group implements Movable { getPosition(): PositionInterface { return { x: this.x, - y: this.y + y: this.y, }; } @@ -83,7 +87,7 @@ export class Group implements Movable { if (oldX === undefined) { this.positionNotifier.enter(this); } 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; } - join(user: User): void - { + join(user: User): void { // Broadcast on the right event this.connectCallback(user, this); this.users.add(user); user.group = this; } - leave(user: User): void - { + leave(user: User): void { const success = this.users.delete(user); 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; @@ -123,8 +125,7 @@ export class Group implements Movable { * Let's kick everybody out. * Usually used when there is only one user left. */ - destroy(): void - { + destroy(): void { if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId); for (const user of this.users) { this.leave(user); @@ -132,7 +133,7 @@ export class Group implements Movable { this.wasDestroyed = true; } - get getSize(){ + get getSize() { return this.users.size; } } diff --git a/back/src/Model/Movable.ts b/back/src/Model/Movable.ts index 173db0ae..ca586b7c 100644 --- a/back/src/Model/Movable.ts +++ b/back/src/Model/Movable.ts @@ -1,8 +1,8 @@ -import {PositionInterface} from "_Model/PositionInterface"; +import { PositionInterface } from "_Model/PositionInterface"; /** * A physical object that can be placed into a Zone */ export interface Movable { - getPosition(): PositionInterface + getPosition(): PositionInterface; } diff --git a/back/src/Model/PositionInterface.ts b/back/src/Model/PositionInterface.ts index d3b0dd47..65636759 100644 --- a/back/src/Model/PositionInterface.ts +++ b/back/src/Model/PositionInterface.ts @@ -1,4 +1,4 @@ export interface PositionInterface { - x: number, - y: number + x: number; + y: number; } diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index 275bf9d0..c34c1ef1 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -8,12 +8,12 @@ * 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. */ -import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone"; -import {Movable} from "_Model/Movable"; -import {PositionInterface} from "_Model/PositionInterface"; -import {ZoneSocket} from "../RoomManager"; -import {User} from "_Model/User"; -import {EmoteEventMessage} from "../Messages/generated/messages_pb"; +import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone"; +import { Movable } from "_Model/Movable"; +import { PositionInterface } from "_Model/PositionInterface"; +import { ZoneSocket } from "../RoomManager"; +import { User } from "_Model/User"; +import { EmoteEventMessage } from "../Messages/generated/messages_pb"; interface ZoneDescriptor { i: number; @@ -21,19 +21,24 @@ interface ZoneDescriptor { } export class PositionNotifier { - // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) 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 { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), - } + }; } public enter(thing: Movable): void { @@ -100,6 +105,5 @@ export class PositionNotifier { const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); const zone = this.getZone(zoneDesc.i, zoneDesc.j); zone.emitEmoteEvent(emoteEventMessage); - } } diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts index 3ac62bca..d1de8800 100644 --- a/back/src/Model/RoomIdentifier.ts +++ b/back/src/Model/RoomIdentifier.ts @@ -1,30 +1,30 @@ //helper functions to parse room IDs export const isRoomAnonymous = (roomID: string): boolean => { - if (roomID.startsWith('_/')) { + if (roomID.startsWith("_/")) { return true; - } else if(roomID.startsWith('@/')) { + } else if (roomID.startsWith("@/")) { return false; } else { - throw new Error('Incorrect room ID: '+roomID); + throw new Error("Incorrect room ID: " + roomID); } -} +}; export const extractRoomSlugPublicRoomId = (roomId: string): string => { - const idParts = roomId.split('/'); - if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); - return idParts.slice(2).join('/'); -} + const idParts = roomId.split("/"); + if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId); + return idParts.slice(2).join("/"); +}; export interface extractDataFromPrivateRoomIdResponse { organizationSlug: string; worldSlug: string; roomSlug: string; } export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { - const idParts = roomId.split('/'); - if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const idParts = roomId.split("/"); + if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId); const organizationSlug = idParts[1]; const worldSlug = idParts[2]; const roomSlug = idParts[3]; - return {organizationSlug, worldSlug, roomSlug} -} \ No newline at end of file + return { organizationSlug, worldSlug, roomSlug }; +}; diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index 4a3e75ec..186fb32a 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -1,11 +1,17 @@ import { Group } from "./Group"; import { PointInterface } from "./Websocket/PointInterface"; -import {Zone} from "_Model/Zone"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {ServerDuplexStream} from "grpc"; -import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; +import { Zone } from "_Model/Zone"; +import { Movable } from "_Model/Movable"; +import { PositionNotifier } from "_Model/PositionNotifier"; +import { ServerDuplexStream } from "grpc"; +import { + BatchMessage, + CompanionMessage, + PusherToBackMessage, + ServerToClientMessage, + SubMessage, +} from "../Messages/generated/messages_pb"; +import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; export type UserSocket = ServerDuplexStream; @@ -22,7 +28,7 @@ export class User implements Movable { private positionNotifier: PositionNotifier, public readonly socket: UserSocket, public readonly tags: string[], - public readonly visitCardUrl: string|null, + public readonly visitCardUrl: string | null, public readonly name: string, public readonly characterLayers: CharacterLayer[], public readonly companion?: CompanionMessage @@ -42,9 +48,8 @@ export class User implements Movable { this.positionNotifier.updatePosition(this, position, oldPosition); } - private batchedMessages: BatchMessage = new BatchMessage(); - private batchTimeout: NodeJS.Timeout|null = null; + private batchTimeout: NodeJS.Timeout | null = null; public emitInBatch(payload: SubMessage): void { this.batchedMessages.addPayload(payload); diff --git a/back/src/Model/Websocket/CharacterLayer.ts b/back/src/Model/Websocket/CharacterLayer.ts index 13d838ee..3e428790 100644 --- a/back/src/Model/Websocket/CharacterLayer.ts +++ b/back/src/Model/Websocket/CharacterLayer.ts @@ -1,4 +1,4 @@ export interface CharacterLayer { - name: string, - url: string|undefined + name: string; + url: string | undefined; } diff --git a/back/src/Model/Websocket/ItemEventMessage.ts b/back/src/Model/Websocket/ItemEventMessage.ts index b1f9203e..1bb7f615 100644 --- a/back/src/Model/Websocket/ItemEventMessage.ts +++ b/back/src/Model/Websocket/ItemEventMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isItemEventMessageInterface = - new tg.IsInterface().withProperties({ +export const isItemEventMessageInterface = new tg.IsInterface() + .withProperties({ itemId: tg.isNumber, event: tg.isString, state: tg.isUnknown, parameters: tg.isUnknown, - }).get(); + }) + .get(); export type ItemEventMessageInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/MessageUserPosition.ts b/back/src/Model/Websocket/MessageUserPosition.ts index ee43d58c..19b57d2e 100644 --- a/back/src/Model/Websocket/MessageUserPosition.ts +++ b/back/src/Model/Websocket/MessageUserPosition.ts @@ -1,7 +1,10 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; -export class Point implements PointInterface{ - constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { - } +export class Point implements PointInterface { + constructor( + public x: number, + public y: number, + public direction: string = "none", + public moving: boolean = false + ) {} } - diff --git a/back/src/Model/Websocket/PointInterface.ts b/back/src/Model/Websocket/PointInterface.ts index afb07a23..d7c7826e 100644 --- a/back/src/Model/Websocket/PointInterface.ts +++ b/back/src/Model/Websocket/PointInterface.ts @@ -7,11 +7,12 @@ import * as tg from "generic-type-guard"; readonly moving: boolean; }*/ -export const isPointInterface = - new tg.IsInterface().withProperties({ +export const isPointInterface = new tg.IsInterface() + .withProperties({ x: tg.isNumber, y: tg.isNumber, direction: tg.isString, - moving: tg.isBoolean - }).get(); + moving: tg.isBoolean, + }) + .get(); export type PointInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/ProtobufUtils.ts b/back/src/Model/Websocket/ProtobufUtils.ts index b85a4257..68817a4f 100644 --- a/back/src/Model/Websocket/ProtobufUtils.ts +++ b/back/src/Model/Websocket/ProtobufUtils.ts @@ -1,34 +1,33 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; import { CharacterLayerMessage, ItemEventMessage, PointMessage, - PositionMessage + PositionMessage, } from "../../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; +import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; import Direction = PositionMessage.Direction; -import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; -import {PositionInterface} from "_Model/PositionInterface"; +import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; +import { PositionInterface } from "_Model/PositionInterface"; export class ProtobufUtils { - public static toPositionMessage(point: PointInterface): PositionMessage { let direction: Direction; switch (point.direction) { - case 'up': + case "up": direction = Direction.UP; break; - case 'down': + case "down": direction = Direction.DOWN; break; - case 'left': + case "left": direction = Direction.LEFT; break; - case 'right': + case "right": direction = Direction.RIGHT; break; default: - throw new Error('unexpected direction'); + throw new Error("unexpected direction"); } const position = new PositionMessage(); @@ -44,16 +43,16 @@ export class ProtobufUtils { let direction: string; switch (position.getDirection()) { case Direction.UP: - direction = 'up'; + direction = "up"; break; case Direction.DOWN: - direction = 'down'; + direction = "down"; break; case Direction.LEFT: - direction = 'left'; + direction = "left"; break; case Direction.RIGHT: - direction = 'right'; + direction = "right"; break; default: throw new Error("Unexpected direction"); @@ -82,7 +81,7 @@ export class ProtobufUtils { event: itemEventMessage.getEvent(), parameters: JSON.parse(itemEventMessage.getParametersjson()), state: JSON.parse(itemEventMessage.getStatejson()), - } + }; } public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { @@ -96,7 +95,7 @@ export class ProtobufUtils { } public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { - return characterLayers.map(function(characterLayer): CharacterLayerMessage { + return characterLayers.map(function (characterLayer): CharacterLayerMessage { const message = new CharacterLayerMessage(); message.setName(characterLayer.name); if (characterLayer.url) { @@ -107,7 +106,7 @@ export class ProtobufUtils { } public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] { - return characterLayers.map(function(characterLayer): CharacterLayer { + return characterLayers.map(function (characterLayer): CharacterLayer { const url = characterLayer.getUrl(); return { name: characterLayer.getName(), diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index ffb172bb..d236e489 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -1,35 +1,52 @@ -import {User} from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {Movable} from "./Movable"; -import {Group} from "./Group"; -import {ZoneSocket} from "../RoomManager"; -import {EmoteEventMessage} from "../Messages/generated/messages_pb"; +import { User } from "./User"; +import { PositionInterface } from "_Model/PositionInterface"; +import { Movable } from "./Movable"; +import { Group } from "./Group"; +import { ZoneSocket } from "../RoomManager"; +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 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 class Zone { private things: Set = new Set(); private listeners: Set = new Set(); - - - constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { } + + constructor( + 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 */ - public leave(thing: Movable, newZone: Zone|null) { + public leave(thing: Movable, newZone: Zone | null) { const result = this.things.delete(thing); if (!result) { 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) { - 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); } @@ -37,13 +54,13 @@ export class Zone { /** * 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) { 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.notifyEnter(thing, oldZone, position); } @@ -51,13 +68,12 @@ export class Zone { /** * 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) { this.onEnters(thing, oldZone, listener); } } - public move(thing: Movable, position: PositionInterface) { if (!this.things.has(thing)) { this.things.add(thing); @@ -67,7 +83,7 @@ export class Zone { for (const listener of this.listeners) { //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) { this.onEmote(emoteEventMessage, listener); } - } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 19266687..9aaf1edb 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -1,4 +1,4 @@ -import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb"; +import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb"; import { AdminGlobalMessage, AdminMessage, @@ -11,92 +11,114 @@ import { JoinRoomMessage, PlayGlobalMessage, PusherToBackMessage, - QueryJitsiJwtMessage, RefreshRoomPromptMessage, + QueryJitsiJwtMessage, + RefreshRoomPromptMessage, ServerToAdminClientMessage, ServerToClientMessage, SilentMessage, UserMovesMessage, - WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, - ZoneMessage + WebRtcSignalToServerMessage, + WorldFullWarningToRoomMessage, + ZoneMessage, } from "./Messages/generated/messages_pb"; -import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc"; -import {socketManager} from "./Services/SocketManager"; -import {emitError} from "./Services/MessageHelpers"; -import {User, UserSocket} from "./Model/User"; -import {GameRoom} from "./Model/GameRoom"; +import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc"; +import { socketManager } from "./Services/SocketManager"; +import { emitError } from "./Services/MessageHelpers"; +import { User, UserSocket } from "./Model/User"; +import { GameRoom } from "./Model/GameRoom"; 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; export type ZoneSocket = ServerWritableStream; const roomManager: IRoomManagerServer = { joinRoom: (call: UserSocket): void => { - console.log('joinRoom called'); + console.log("joinRoom called"); - let room: GameRoom|null = null; - let user: User|null = null; + let room: GameRoom | null = null; + let user: User | null = null; - call.on('data', (message: PusherToBackMessage) => { + call.on("data", (message: PusherToBackMessage) => { try { if (room === null || user === null) { if (message.hasJoinroommessage()) { - socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => { - if (call.writable) { - room = gameRoom; - user = myUser; - } else { - //Connexion may have been closed before the init was finished, so we have to manually disconnect the user. - socketManager.leaveRoom(gameRoom, myUser); - } - }); + socketManager + .handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage) + .then(({ room: gameRoom, user: myUser }) => { + if (call.writable) { + room = gameRoom; + user = 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 { - 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 { if (message.hasJoinroommessage()) { - throw new Error('Cannot call JoinRoomMessage twice!'); + throw new Error("Cannot call JoinRoomMessage twice!"); } else if (message.hasUsermovesmessage()) { - socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage); + socketManager.handleUserMovesMessage( + room, + user, + message.getUsermovesmessage() as UserMovesMessage + ); } else if (message.hasSilentmessage()) { socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitVideo( + room, + user, + message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitScreenSharing( + room, + user, + message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasPlayglobalmessage()) { socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); - } else if (message.hasQueryjitsijwtmessage()){ - socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); - } else if (message.hasEmotepromptmessage()){ - socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage); - }else if (message.hasSendusermessage()) { + } else if (message.hasQueryjitsijwtmessage()) { + socketManager.handleQueryJitsiJwtMessage( + user, + message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage + ); + } else if (message.hasEmotepromptmessage()) { + socketManager.handleEmoteEventMessage( + room, + user, + message.getEmotepromptmessage() as EmotePromptMessage + ); + } else if (message.hasSendusermessage()) { const sendUserMessage = message.getSendusermessage(); - if(sendUserMessage !== undefined) { + if (sendUserMessage !== undefined) { socketManager.handlerSendUserMessage(user, sendUserMessage); } - }else if (message.hasBanusermessage()) { + } else if (message.hasBanusermessage()) { const banUserMessage = message.getBanusermessage(); - if(banUserMessage !== undefined) { + if (banUserMessage !== undefined) { socketManager.handlerBanUserMessage(room, user, banUserMessage); } } else { - throw new Error('Unhandled message type'); + throw new Error("Unhandled message type"); } } } catch (e) { emitError(call, e); call.end(); } - }); - call.on('end', () => { - debug('joinRoom ended'); + call.on("end", () => { + debug("joinRoom ended"); if (user !== null && room !== null) { socketManager.leaveRoom(room, user); } @@ -105,41 +127,40 @@ const roomManager: IRoomManagerServer = { user = null; }); - call.on('error', (err: Error) => { - console.error('An error occurred in joinRoom stream:', err); + call.on("error", (err: Error) => { + console.error("An error occurred in joinRoom stream:", err); }); - }, listenZone(call: ZoneSocket): void { - debug('listenZone called'); + debug("listenZone called"); const zoneMessage = call.request; socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); - call.on('cancelled', () => { - debug('listenZone cancelled'); + call.on("cancelled", () => { + debug("listenZone cancelled"); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); - }) - - call.on('close', () => { - debug('listenZone connection closed'); + }); + + call.on("close", () => { + debug("listenZone connection closed"); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); - }).on('error', (e) => { - console.error('An error occurred in listenZone stream:', e); + }).on("error", (e) => { + console.error("An error occurred in listenZone stream:", e); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); }); }, adminRoom(call: AdminSocket): void { - console.log('adminRoom called'); + console.log("adminRoom called"); 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 { if (room === null) { if (message.hasSubscribetoroom()) { @@ -148,18 +169,17 @@ const roomManager: IRoomManagerServer = { room = gameRoom; }); } 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) { emitError(call, e); call.end(); } - }); - call.on('end', () => { - debug('joinRoom ended'); + call.on("end", () => { + debug("joinRoom ended"); if (room !== null) { socketManager.leaveAdminRoom(room, admin); } @@ -167,18 +187,21 @@ const roomManager: IRoomManagerServer = { room = null; }); - call.on('error', (err: Error) => { - console.error('An error occurred in joinAdminRoom stream:', err); + call.on("error", (err: Error) => { + console.error("An error occurred in joinAdminRoom stream:", err); }); }, sendAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - - socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage()); + socketManager.sendAdminMessage( + call.request.getRoomid(), + call.request.getRecipientuuid(), + call.request.getMessage() + ); callback(null, new EmptyMessage()); }, sendGlobalAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - throw new Error('Not implemented yet'); + throw new Error("Not implemented yet"); // TODO callback(null, new EmptyMessage()); }, @@ -192,14 +215,20 @@ const roomManager: IRoomManagerServer = { socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()); callback(null, new EmptyMessage()); }, - sendWorldFullWarningToRoom(call: ServerUnaryCall, callback: sendUnaryData): void { + sendWorldFullWarningToRoom( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { socketManager.dispatchWorlFullWarning(call.request.getRoomid()); callback(null, new EmptyMessage()); }, - sendRefreshRoomPrompt(call: ServerUnaryCall, callback: sendUnaryData): void { + sendRefreshRoomPrompt( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { socketManager.dispatchRoomRefresh(call.request.getRoomid()); callback(null, new EmptyMessage()); }, }; -export {roomManager}; +export { roomManager }; diff --git a/back/src/Server/server/app.ts b/back/src/Server/server/app.ts index 3b98a9b3..4c422d5c 100644 --- a/back/src/Server/server/app.ts +++ b/back/src/Server/server/app.ts @@ -1,13 +1,13 @@ -import { App as _App, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { App as _App, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class App extends (_App) { - constructor(options: AppOptions = {}) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions = {}) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default App; diff --git a/back/src/Server/server/baseapp.ts b/back/src/Server/server/baseapp.ts index accd8a99..6d973ac7 100644 --- a/back/src/Server/server/baseapp.ts +++ b/back/src/Server/server/baseapp.ts @@ -1,116 +1,109 @@ -import { Readable } from 'stream'; -import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { Readable } from "stream"; +import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; -import formData from './formdata'; -import { stob } from './utils'; -import { Handler } from './types'; -import {join} from "path"; +import formData from "./formdata"; +import { stob } from "./utils"; +import { Handler } from "./types"; +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 handleBody = (res: HttpResponse, req: HttpRequest) => { - const contType = req.getHeader('content-type'); + const contType = req.getHeader("content-type"); - res.bodyStream = function() { - const stream = new Readable(); - stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method + res.bodyStream = function () { + const stream = new Readable(); + stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method - this.onData((ab: ArrayBuffer, isLast: boolean) => { - // 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 - if (isLast) { - stream.push(null); - } - }); + this.onData((ab: ArrayBuffer, isLast: boolean) => { + // 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 + if (isLast) { + stream.push(null); + } + }); - return stream; - }; + return stream; + }; - res.body = () => stob(res.bodyStream()); + res.body = () => stob(res.bodyStream()); - if (contType.includes('application/json')) - res.json = async () => JSON.parse(await res.body()); - if (contTypes.map(t => contType.includes(t)).includes(true)) - res.formData = formData.bind(res, contType); + if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body()); + if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType); }; class BaseApp { - _sockets = new Map(); - ws!: TemplatedApp['ws']; - get!: TemplatedApp['get']; - _post!: TemplatedApp['post']; - _put!: TemplatedApp['put']; - _patch!: TemplatedApp['patch']; - _listen!: TemplatedApp['listen']; + _sockets = new Map(); + ws!: TemplatedApp["ws"]; + get!: TemplatedApp["get"]; + _post!: TemplatedApp["post"]; + _put!: TemplatedApp["put"]; + _patch!: TemplatedApp["patch"]; + _listen!: TemplatedApp["listen"]; - post(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._post(pattern, (res, req) => { - handleBody(res, req); - handler(res, req); - }); - return this; - } + post(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._post(pattern, (res, req) => { + handleBody(res, req); + handler(res, req); + }); + return this; + } - put(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._put(pattern, (res, req) => { - handleBody(res, req); + put(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._put(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - patch(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._patch(pattern, (res, req) => { - handleBody(res, req); + patch(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._patch(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - listen(h: string | number, p: Function | number = noOp, cb?: Function) { - if (typeof p === 'number' && typeof h === 'string') { - this._listen(h, p, socket => { - this._sockets.set(p, socket); - if (cb === undefined) { - throw new Error('cb undefined'); + listen(h: string | number, p: Function | number = noOp, cb?: Function) { + if (typeof p === "number" && typeof h === "string") { + this._listen(h, p, (socket) => { + this._sockets.set(p, socket); + if (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); - }); - } 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; } - return this; - } - - close(port: null | number = null) { - if (port) { - this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); - this._sockets.delete(port); - } else { - this._sockets.forEach(app => { - us_listen_socket_close(app); - }); - this._sockets.clear(); + close(port: null | number = null) { + if (port) { + this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); + this._sockets.delete(port); + } else { + this._sockets.forEach((app) => { + us_listen_socket_close(app); + }); + this._sockets.clear(); + } + return this; } - return this; - } } export default BaseApp; diff --git a/back/src/Server/server/formdata.ts b/back/src/Server/server/formdata.ts index 9dd08440..66e51db4 100644 --- a/back/src/Server/server/formdata.ts +++ b/back/src/Server/server/formdata.ts @@ -1,100 +1,99 @@ -import { createWriteStream } from 'fs'; -import { join, dirname } from 'path'; -import Busboy from 'busboy'; -import mkdirp from 'mkdirp'; +import { createWriteStream } from "fs"; +import { join, dirname } from "path"; +import Busboy from "busboy"; +import mkdirp from "mkdirp"; function formData( - contType: string, - options: busboy.BusboyConfig & { - abortOnLimit?: boolean; - tmpDir?: string; - onFile?: ( - fieldname: string, - file: NodeJS.ReadableStream, - filename: string, - encoding: string, - mimetype: string - ) => string; - onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - filename?: (oldName: string) => string; - } = {} + contType: string, + options: busboy.BusboyConfig & { + abortOnLimit?: boolean; + tmpDir?: string; + onFile?: ( + fieldname: string, + file: NodeJS.ReadableStream, + filename: string, + encoding: string, + mimetype: string + ) => string; + onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + filename?: (oldName: string) => string; + } = {} ) { - console.log('Enter form data'); - options.headers = { - 'content-type': contType - }; + console.log("Enter form data"); + options.headers = { + "content-type": contType, + }; - return new Promise((resolve, reject) => { - const busb = new Busboy(options); - const ret = {}; + return new Promise((resolve, reject) => { + const busb = new Busboy(options); + const ret = {}; - this.bodyStream().pipe(busb); + this.bodyStream().pipe(busb); - busb.on('limit', () => { - if (options.abortOnLimit) { - reject(Error('limit')); - } + busb.on("limit", () => { + if (options.abortOnLimit) { + 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( - ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any - fieldname: string, - value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any + ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + fieldname: string, + value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any ) { - if (fieldname.endsWith('[]')) { - fieldname = fieldname.slice(0, fieldname.length - 2); - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); + if (fieldname.endsWith("[]")) { + fieldname = fieldname.slice(0, fieldname.length - 2); + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else { + ret[fieldname] = [value]; + } } 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; diff --git a/back/src/Server/server/sslapp.ts b/back/src/Server/server/sslapp.ts index 46ae89a5..80df0e4a 100644 --- a/back/src/Server/server/sslapp.ts +++ b/back/src/Server/server/sslapp.ts @@ -1,13 +1,13 @@ -import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class SSLApp extends (_SSLApp) { - constructor(options: AppOptions) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default SSLApp; diff --git a/back/src/Server/server/types.ts b/back/src/Server/server/types.ts index 3d0f48c7..afc21d17 100644 --- a/back/src/Server/server/types.ts +++ b/back/src/Server/server/types.ts @@ -1,9 +1,9 @@ -import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; export type UwsApp = { - (options: AppOptions): TemplatedApp; - new (options: AppOptions): TemplatedApp; - prototype: TemplatedApp; + (options: AppOptions): TemplatedApp; + new (options: AppOptions): TemplatedApp; + prototype: TemplatedApp; }; export type Handler = (res: HttpResponse, req: HttpRequest) => void; diff --git a/back/src/Server/server/utils.ts b/back/src/Server/server/utils.ts index 80ea3938..dc813064 100644 --- a/back/src/Server/server/utils.ts +++ b/back/src/Server/server/utils.ts @@ -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 - const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( - Object.keys(from) - ); - ownProps.forEach(prop => { - if (prop === 'constructor' || from[prop] === undefined) return; - if (who[prop] && overwrite) { - who[`_${prop}`] = who[prop]; - } - if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); - else who[prop] = from[prop]; - }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extend(who: any, from: any, overwrite = true) { + const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); + ownProps.forEach((prop) => { + if (prop === "constructor" || from[prop] === undefined) return; + if (who[prop] && overwrite) { + who[`_${prop}`] = who[prop]; + } + if (typeof from[prop] === "function") who[prop] = from[prop].bind(who); + else who[prop] = from[prop]; + }); } function stob(stream: ReadStream): Promise { - return new Promise(resolve => { - const buffers: Buffer[] = []; - stream.on('data', buffers.push.bind(buffers)); + return new Promise((resolve) => { + const buffers: Buffer[] = []; + stream.on("data", buffers.push.bind(buffers)); - stream.on('end', () => { - switch (buffers.length) { - case 0: - resolve(Buffer.allocUnsafe(0)); - break; - case 1: - resolve(buffers[0]); - break; - default: - resolve(Buffer.concat(buffers)); - } + stream.on("end", () => { + switch (buffers.length) { + case 0: + resolve(Buffer.allocUnsafe(0)); + break; + case 1: + resolve(buffers[0]); + break; + default: + resolve(Buffer.concat(buffers)); + } + }); }); - }); } export { extend, stob }; diff --git a/back/src/Server/sifrr.server.ts b/back/src/Server/sifrr.server.ts index 47fba02c..4ef03721 100644 --- a/back/src/Server/sifrr.server.ts +++ b/back/src/Server/sifrr.server.ts @@ -1,19 +1,19 @@ -import { parse } from 'query-string'; -import { HttpRequest } from 'uWebSockets.js'; -import App from './server/app'; -import SSLApp from './server/sslapp'; -import * as types from './server/types'; +import { parse } from "query-string"; +import { HttpRequest } from "uWebSockets.js"; +import App from "./server/app"; +import SSLApp from "./server/sslapp"; +import * as types from "./server/types"; const getQuery = (req: HttpRequest) => { - return parse(req.getQuery()); + return parse(req.getQuery()); }; export { App, SSLApp, getQuery }; -export * from './server/types'; +export * from "./server/types"; export default { - App, - SSLApp, - getQuery, - ...types + App, + SSLApp, + getQuery, + ...types, }; diff --git a/back/src/Services/ArrayHelper.ts b/back/src/Services/ArrayHelper.ts index 67321d1b..8af1da9f 100644 --- a/back/src/Services/ArrayHelper.ts +++ b/back/src/Services/ArrayHelper.ts @@ -1,3 +1,3 @@ -export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { - return array1.filter(value => array2.includes(value)).length > 0; -} \ No newline at end of file +export const arrayIntersect = (array1: string[], array2: string[]): boolean => { + return array1.filter((value) => array2.includes(value)).length > 0; +}; diff --git a/back/src/Services/ClientEventsEmitter.ts b/back/src/Services/ClientEventsEmitter.ts index 381137a1..0f56d55c 100644 --- a/back/src/Services/ClientEventsEmitter.ts +++ b/back/src/Services/ClientEventsEmitter.ts @@ -1,7 +1,7 @@ -const EventEmitter = require('events'); +const EventEmitter = require("events"); -const clientJoinEvent = 'clientJoin'; -const clientLeaveEvent = 'clientLeave'; +const clientJoinEvent = "clientJoin"; +const clientLeaveEvent = "clientLeave"; class ClientEventsEmitter extends EventEmitter { emitClientJoin(clientUUid: string, roomId: string): void { diff --git a/back/src/Services/CpuTracker.ts b/back/src/Services/CpuTracker.ts index c7d57f3d..3d06ca70 100644 --- a/back/src/Services/CpuTracker.ts +++ b/back/src/Services/CpuTracker.ts @@ -1,6 +1,6 @@ -import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; +import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable"; -function secNSec2ms(secNSec: Array|number) { +function secNSec2ms(secNSec: Array | number) { if (Array.isArray(secNSec)) { return secNSec[0] * 1000 + secNSec[1] / 1000000; } @@ -12,17 +12,17 @@ class CpuTracker { private overHeating: boolean = false; constructor() { - let time = process.hrtime.bigint() - let usage = process.cpuUsage() + let time = process.hrtime.bigint(); + let usage = process.cpuUsage(); setInterval(() => { const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage) - usage = process.cpuUsage() + const elapUsage = process.cpuUsage(usage); + usage = process.cpuUsage(); const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user) - const elapSystMS = secNSec2ms(elapUsage.system) - this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) + const elapUserMS = secNSec2ms(elapUsage.user); + const elapSystMS = secNSec2ms(elapUsage.system); + this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000); time = elapTime; diff --git a/back/src/Services/GaugeManager.ts b/back/src/Services/GaugeManager.ts index 80712856..6d2183d8 100644 --- a/back/src/Services/GaugeManager.ts +++ b/back/src/Services/GaugeManager.ts @@ -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 class GaugeManager { @@ -10,29 +10,29 @@ class GaugeManager { constructor() { this.nbRoomsGauge = new Gauge({ - name: 'workadventure_nb_rooms', - help: 'Number of active rooms' + name: "workadventure_nb_rooms", + help: "Number of active rooms", }); this.nbClientsGauge = new Gauge({ - name: 'workadventure_nb_sockets', - help: 'Number of connected sockets', - labelNames: [ ] + name: "workadventure_nb_sockets", + help: "Number of connected sockets", + labelNames: [], }); this.nbClientsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_clients_per_room', - help: 'Number of clients per room', - labelNames: [ 'room' ] + name: "workadventure_nb_clients_per_room", + help: "Number of clients per room", + labelNames: ["room"], }); this.nbGroupsPerRoomCounter = new Counter({ - name: 'workadventure_counter_groups_per_room', - help: 'Counter of groups per room', - labelNames: [ 'room' ] + name: "workadventure_counter_groups_per_room", + help: "Counter of groups per room", + labelNames: ["room"], }); this.nbGroupsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_groups_per_room', - help: 'Number of groups per room', - labelNames: [ 'room' ] + name: "workadventure_nb_groups_per_room", + help: "Number of groups per room", + labelNames: ["room"], }); } @@ -54,13 +54,13 @@ class GaugeManager { } incNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomCounter.inc({ room: roomId }) - this.nbGroupsPerRoomGauge.inc({ room: roomId }) + this.nbGroupsPerRoomCounter.inc({ room: roomId }); + this.nbGroupsPerRoomGauge.inc({ room: roomId }); } - + decNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomGauge.dec({ room: roomId }) + this.nbGroupsPerRoomGauge.dec({ room: roomId }); } } -export const gaugeManager = new GaugeManager(); \ No newline at end of file +export const gaugeManager = new GaugeManager(); diff --git a/back/src/Services/MessageHelpers.ts b/back/src/Services/MessageHelpers.ts index b2600a4a..493f7173 100644 --- a/back/src/Services/MessageHelpers.ts +++ b/back/src/Services/MessageHelpers.ts @@ -1,5 +1,5 @@ -import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb"; -import {UserSocket} from "_Model/User"; +import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb"; +import { UserSocket } from "_Model/User"; export function emitError(Client: UserSocket, message: string): void { const errorMessage = new ErrorMessage(); @@ -9,7 +9,7 @@ export function emitError(Client: UserSocket, message: string): void { serverToClientMessage.setErrormessage(errorMessage); //if (!Client.disconnecting) { - Client.write(serverToClientMessage); + Client.write(serverToClientMessage); //} console.warn(message); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index a56a1ac4..e61763cd 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -1,4 +1,4 @@ -import {GameRoom} from "../Model/GameRoom"; +import { GameRoom } from "../Model/GameRoom"; import { ItemEventMessage, ItemStateMessage, @@ -27,39 +27,39 @@ import { WorldFullWarningMessage, UserLeftZoneMessage, EmoteEventMessage, - BanUserMessage, RefreshRoomMessage, EmotePromptMessage, + BanUserMessage, + RefreshRoomMessage, + EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {User, UserSocket} from "../Model/User"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {Group} from "../Model/Group"; -import {cpuTracker} from "./CpuTracker"; +import { User, UserSocket } from "../Model/User"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { Group } from "../Model/Group"; +import { cpuTracker } from "./CpuTracker"; import { GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY, - TURN_STATIC_AUTH_SECRET + TURN_STATIC_AUTH_SECRET, } from "../Enum/EnvironmentVariable"; -import {Movable} from "../Model/Movable"; -import {PositionInterface} from "../Model/PositionInterface"; +import { Movable } from "../Model/Movable"; +import { PositionInterface } from "../Model/PositionInterface"; import Jwt from "jsonwebtoken"; -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {clientEventsEmitter} from "./ClientEventsEmitter"; -import {gaugeManager} from "./GaugeManager"; -import {ZoneSocket} from "../RoomManager"; -import {Zone} from "_Model/Zone"; +import { JITSI_URL } from "../Enum/EnvironmentVariable"; +import { clientEventsEmitter } from "./ClientEventsEmitter"; +import { gaugeManager } from "./GaugeManager"; +import { ZoneSocket } from "../RoomManager"; +import { Zone } from "_Model/Zone"; import Debug from "debug"; -import {Admin} from "_Model/Admin"; +import { Admin } from "_Model/Admin"; import crypto from "crypto"; - -const debug = Debug('sockermanager'); +const debug = Debug("sockermanager"); function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void { // TODO: should we batch those every 100ms? const batchMessage = new BatchToPusherMessage(); batchMessage.addPayload(subMessage); - socket.write(batchMessage); } @@ -68,7 +68,6 @@ export class SocketManager { private rooms: Map = new Map(); constructor() { - clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { 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 - const {room, user} = await this.joinRoom(socket, joinRoomMessage); - + const { room, user } = await this.joinRoom(socket, joinRoomMessage); + if (!socket.writable) { - console.warn('Socket was aborted'); + console.warn("Socket was aborted"); return { room, - user + user, }; } const roomJoinedMessage = new RoomJoinedMessage(); @@ -108,9 +109,8 @@ export class SocketManager { return { room, - user + user, }; - } handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) { @@ -124,13 +124,12 @@ export class SocketManager { } if (position === undefined) { - throw new Error('Position not found in message'); + throw new Error("Position not found in message"); } const viewport = userMoves.viewport; if (viewport === undefined) { - throw new Error('Viewport not found in message'); + throw new Error("Viewport not found in message"); } - // update position in the world room.updatePosition(user, ProtobufUtils.toPointInterface(position)); @@ -189,7 +188,11 @@ export class SocketManager { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); 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; } @@ -197,8 +200,8 @@ export class SocketManager { webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcpassword(password); } @@ -207,7 +210,7 @@ export class SocketManager { serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); + remoteUser.socket.write(serverToClientMessage); //} } @@ -215,7 +218,11 @@ export class SocketManager { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); 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; } @@ -223,8 +230,8 @@ export class SocketManager { webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcpassword(password); } @@ -233,11 +240,11 @@ export class SocketManager { serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); //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 try { //user leave previous world @@ -249,33 +256,39 @@ export class SocketManager { } } finally { clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); - console.log('A user left'); + console.log("A user left"); } } async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room - let world = this.rooms.get(roomId) - if(world === undefined){ + let world = this.rooms.get(roomId); + if (world === undefined) { world = new GameRoom( roomId, (user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.disConnectedUser(user, group), MINIMUM_DISTANCE, GROUP_RADIUS, - (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener), - (thing: Movable, position:PositionInterface, listener: ZoneSocket) => 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), + (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => + this.onZoneEnter(thing, fromZone, listener), + (thing: Movable, position: PositionInterface, listener: ZoneSocket) => + 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(); 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 room = await socketManager.getOrCreateRoom(roomId); @@ -284,15 +297,15 @@ export class SocketManager { const user = room.join(socket, joinRoomMessage); clientEventsEmitter.emitClientJoin(user.uuid, roomId); - console.log(new Date().toISOString() + ' A user joined'); - return {room, user}; + console.log(new Date().toISOString() + " A user joined"); + 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) { const userJoinedZoneMessage = new UserJoinedZoneMessage(); 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.setName(thing.name); @@ -312,11 +325,11 @@ export class SocketManager { } else if (thing instanceof Group) { this.emitCreateUpdateGroupEvent(listener, fromZone, thing); } 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) { const userMovedMessage = new UserMovedMessage(); userMovedMessage.setUserid(thing.id); @@ -331,21 +344,20 @@ export class SocketManager { } else if (thing instanceof Group) { this.emitCreateUpdateGroupEvent(listener, null, thing); } 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) { this.emitUserLeftEvent(listener, thing.id, newZone); } else if (thing instanceof Group) { this.emitDeleteGroupEvent(listener, thing.getId(), newZone); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) { const subMessage = new SubToPusherMessage(); subMessage.setEmoteeventmessage(emoteEventMessage); @@ -353,7 +365,7 @@ export class SocketManager { 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 pointMessage = new PointMessage(); pointMessage.setX(Math.floor(position.x)); @@ -371,7 +383,7 @@ export class SocketManager { //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(); groupDeleteMessage.setGroupid(groupId); groupDeleteMessage.setTozone(this.toProtoZone(newZone)); @@ -383,7 +395,7 @@ export class SocketManager { //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(); userLeftMessage.setUserid(userId); userLeftMessage.setTozone(this.toProtoZone(newZone)); @@ -394,7 +406,7 @@ export class SocketManager { emitZoneMessage(subMessage, client); } - private toProtoZone(zone: Zone|null): ProtoZone|undefined { + private toProtoZone(zone: Zone | null): ProtoZone | undefined { if (zone !== null) { const zoneMessage = new ProtoZone(); zoneMessage.setX(zone.x); @@ -405,7 +417,6 @@ export class SocketManager { } private joinWebRtcRoom(user: User, group: Group) { - for (const otherUser of group.getUsers()) { if (user === otherUser) { continue; @@ -416,8 +427,8 @@ export class SocketManager { webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setInitiator(true); - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); webrtcStartMessage1.setWebrtcusername(username); webrtcStartMessage1.setWebrtcpassword(password); } @@ -426,16 +437,16 @@ export class SocketManager { serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); //if (!user.socket.disconnecting) { - user.socket.write(serverToClientMessage1); - //console.log('Sending webrtcstart initiator to '+user.socket.userId) + user.socket.write(serverToClientMessage1); + //console.log('Sending webrtcstart initiator to '+user.socket.userId) //} const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setInitiator(false); - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcStartMessage2.setWebrtcusername(username); webrtcStartMessage2.setWebrtcpassword(password); } @@ -444,10 +455,9 @@ export class SocketManager { serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage2); - //console.log('Sending webrtcstart to '+otherUser.socket.userId) + otherUser.socket.write(serverToClientMessage2); + //console.log('Sending webrtcstart to '+otherUser.socket.userId) //} - } } @@ -456,17 +466,17 @@ export class SocketManager { * and the Coturn server. * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey` */ - private getTURNCredentials(name: string, secret: string): {username: string, password: string} { - const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours - const username = [unixTimeStamp, name].join(':'); - const hmac = crypto.createHmac('sha1', secret); - hmac.setEncoding('base64'); + private getTURNCredentials(name: string, secret: string): { username: string; password: string } { + const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours + const username = [unixTimeStamp, name].join(":"); + const hmac = crypto.createHmac("sha1", secret); + hmac.setEncoding("base64"); hmac.write(username); hmac.end(); const password = hmac.read(); return { username: username, - password: password + password: password, }; } @@ -489,10 +499,9 @@ export class SocketManager { serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage1); + otherUser.socket.write(serverToClientMessage1); //} - const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); webrtcDisconnectMessage2.setUserid(otherUser.id); @@ -500,7 +509,7 @@ export class SocketManager { serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); //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(e); } - } public getWorlds(): Map { return this.rooms; } - public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) { 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. - if (SECRET_JITSI_KEY === '') { - throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); + if (SECRET_JITSI_KEY === "") { + 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 const isAdmin = user.tags.includes(tag); - const jwt = Jwt.sign({ - "aud": "jitsi", - "iss": JITSI_ISS, - "sub": JITSI_URL, - "room": room, - "moderator": isAdmin - }, SECRET_JITSI_KEY, { - expiresIn: '1d', - algorithm: "HS256", - header: - { - "alg": "HS256", - "typ": "JWT" - } - }); + const jwt = Jwt.sign( + { + aud: "jitsi", + iss: JITSI_ISS, + sub: JITSI_URL, + room: room, + moderator: isAdmin, + }, + SECRET_JITSI_KEY, + { + expiresIn: "1d", + algorithm: "HS256", + header: { + alg: "HS256", + typ: "JWT", + }, + } + ); const sendJitsiJwtMessage = new SendJitsiJwtMessage(); sendJitsiJwtMessage.setJitsiroom(room); @@ -562,7 +572,7 @@ export class SocketManager { user.socket.write(serverToClientMessage); } - public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){ + public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(sendUserMessageToSend.getMessage()); sendUserMessage.setType(sendUserMessageToSend.getType()); @@ -572,7 +582,7 @@ export class SocketManager { user.socket.write(serverToClientMessage); } - public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){ + public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) { const banUserMessage = new BanUserMessage(); banUserMessage.setMessage(banUserMessageToSend.getMessage()); banUserMessage.setType(banUserMessageToSend.getType()); @@ -592,7 +602,7 @@ export class SocketManager { public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void { const room = this.rooms.get(roomId); 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; } @@ -636,7 +646,7 @@ export class SocketManager { removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) { const room = this.rooms.get(roomId); 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; } @@ -651,7 +661,7 @@ export class SocketManager { return room; } - public leaveAdminRoom(room: GameRoom, admin: Admin){ + public leaveAdminRoom(room: GameRoom, admin: Admin) { room.adminLeave(admin); if (room.isEmpty()) { this.rooms.delete(room.roomId); @@ -663,19 +673,27 @@ export class SocketManager { public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void { const room = this.rooms.get(roomId); 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; } const recipient = room.getUserByUuid(recipientUuid); 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; } const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('ban'); //todo: is the type correct? + sendUserMessage.setType("ban"); //todo: is the type correct? const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setSendusermessage(sendUserMessage); @@ -686,13 +704,21 @@ export class SocketManager { public banUser(roomId: string, recipientUuid: string, message: string): void { const room = this.rooms.get(roomId); 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; } const recipient = room.getUserByUuid(recipientUuid); 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; } @@ -701,7 +727,7 @@ export class SocketManager { const banUserMessage = new BanUserMessage(); banUserMessage.setMessage(message); - banUserMessage.setType('banned'); + banUserMessage.setType("banned"); const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setBanusermessage(banUserMessage); @@ -711,19 +737,22 @@ export class SocketManager { recipient.socket.end(); } - sendAdminRoomMessage(roomId: string, message: string) { const room = this.rooms.get(roomId); if (!room) { //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; } room.getUsers().forEach((recipient) => { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('message'); + sendUserMessage.setType("message"); const clientMessage = new ServerToClientMessage(); clientMessage.setSendusermessage(sendUserMessage); @@ -732,14 +761,18 @@ export class SocketManager { }); } - dispatchWorlFullWarning(roomId: string,): void { + dispatchWorlFullWarning(roomId: string): void { const room = this.rooms.get(roomId); if (!room) { //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; } - + room.getUsers().forEach((recipient) => { 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); if (!room) { return; } - + const versionNumber = room.incrementVersion(); room.getUsers().forEach((recipient) => { const worldFullMessage = new RefreshRoomMessage(); - worldFullMessage.setRoomid(roomId) - worldFullMessage.setVersionnumber(versionNumber) + worldFullMessage.setRoomid(roomId); + worldFullMessage.setVersionnumber(versionNumber); const clientMessage = new ServerToClientMessage(); clientMessage.setRefreshroommessage(worldFullMessage); diff --git a/back/yarn.lock b/back/yarn.lock index 3ac4b0a8..242728db 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -117,6 +117,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" @@ -197,6 +202,14 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv@^6.10.0, ajv@^6.10.2: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" @@ -207,6 +220,11 @@ ajv@^6.10.0, ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-escapes@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" @@ -214,6 +232,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.11.0" +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -241,6 +266,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -325,6 +357,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -389,7 +426,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -475,6 +512,14 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -515,6 +560,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -522,6 +572,14 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -573,11 +631,21 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + colour@~0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -603,6 +671,17 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -614,6 +693,15 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -667,6 +755,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -752,7 +845,14 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -error-ex@^1.2.0: +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -877,6 +977,21 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -1066,11 +1181,21 @@ generic-type-guard@^3.2.0: resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1" integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ== +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -1193,6 +1318,11 @@ http-status-codes@*: resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1220,6 +1350,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -1232,6 +1370,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1402,6 +1545,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -1409,6 +1557,21 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -1467,6 +1630,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1549,6 +1717,45 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +lint-staged@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" + integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + dedent "^0.7.0" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -1610,6 +1817,24 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + long@~3: version "3.2.0" resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" @@ -1661,6 +1886,11 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.2.3: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1685,6 +1915,14 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -1853,6 +2091,13 @@ npm-packlist@^1.1.6: npm-bundled "^1.0.1" npm-normalize-package-bin "^1.0.1" +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -1903,7 +2148,7 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^5.1.0: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -1952,6 +2197,13 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -1966,6 +2218,16 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -1993,6 +2255,11 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -2007,11 +2274,21 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2029,6 +2306,13 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -2039,6 +2323,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -2226,6 +2515,13 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2253,6 +2549,11 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -2290,12 +2591,24 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -signal-exit@^3.0.0, signal-exit@^3.0.2: +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -2309,6 +2622,24 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -2434,6 +2765,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -2469,6 +2805,15 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -2476,6 +2821,15 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -2516,6 +2870,11 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -2587,7 +2946,7 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@^2.3.6: +through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -2703,6 +3062,11 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -2790,6 +3154,13 @@ which@^1.2.9: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -2815,6 +3186,24 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -2842,6 +3231,11 @@ yallist@^3.0.0, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs@^3.10.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index fc38dc41..286f2ac7 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -85,5 +85,5 @@ WA.ui.registerMenuCommand("test", () => { ```
- +
\ No newline at end of file diff --git a/docs/maps/assets/menu-command.png b/docs/maps/assets/menu-command.png deleted file mode 100644 index 0caf75c9..00000000 Binary files a/docs/maps/assets/menu-command.png and /dev/null differ diff --git a/docs/maps/wa-maps.md b/docs/maps/wa-maps.md index 98fdce2d..d7a349c5 100644 --- a/docs/maps/wa-maps.md +++ b/docs/maps/wa-maps.md @@ -60,21 +60,23 @@ By default, the characters can traverse any tiles. If you want to prevent your c To make a tile "collidable", you should: -1. select the relevant tileset and switch to "edit" mode: - {.document-img} - ![](https://workadventu.re/img/docs/collides-1.png) -2. right click on a tile of the tileset to select it: - {.document-img} - ![](https://workadventu.re/img/docs/collides-2.png) -3. on the left pane in the custom properties section, right click and select "Add properties": - {.document-img} - ![](https://workadventu.re/img/docs/collides-3.png) +1. select the relevant tileset and switch to "edit" mode: + + ![](https://workadventu.re/img/docs/collides-1.png){.document-img} + +2. right click on a tile of the tileset to select it: + + ![](https://workadventu.re/img/docs/collides-2.png){.document-img} + +3. on the left pane in the custom properties section, right click and select "Add properties": + + ![](https://workadventu.re/img/docs/collides-3.png){.document-img} Please add a `collides` property. The type of the property must be **bool**. -4. finally, check the checkbox for the `collides` property: - {.document-img} - ![](https://workadventu.re/img/docs/collides-4.png) +4. finally, check the checkbox for the `collides` property: + + ![](https://workadventu.re/img/docs/collides-4.png){.document-img} Repeat for every tile that should be "collidable". diff --git a/front/.gitignore b/front/.gitignore index b6f01e2c..1842041a 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -1,5 +1,9 @@ /node_modules/ -/dist/bundle.js +/dist/*.js +/dist/*.js.map +/dist/*.js.LICENSE.txt +/dist/main.*.css +/dist/main.*.css.map /dist/tests/ /yarn-error.log /dist/webpack.config.js diff --git a/front/.prettierignore b/front/.prettierignore new file mode 100644 index 00000000..1f453464 --- /dev/null +++ b/front/.prettierignore @@ -0,0 +1 @@ +src/Messages/generated diff --git a/front/.prettierrc.json b/front/.prettierrc.json new file mode 100644 index 00000000..e8980d15 --- /dev/null +++ b/front/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "tabWidth": 4 +} diff --git a/front/dist/resources/objects/layout_modes.png b/front/dist/resources/objects/layout_modes.png deleted file mode 100644 index abd9adaf..00000000 Binary files a/front/dist/resources/objects/layout_modes.png and /dev/null differ diff --git a/front/package.json b/front/package.json index 8652ba83..ee031668 100644 --- a/front/package.json +++ b/front/package.json @@ -18,9 +18,11 @@ "fork-ts-checker-webpack-plugin": "^6.2.9", "html-webpack-plugin": "^5.3.1", "jasmine": "^3.5.0", + "lint-staged": "^11.0.0", "mini-css-extract-plugin": "^1.6.0", "node-polyfill-webpack-plugin": "^1.1.2", "npm-run-all": "^4.1.5", + "prettier": "^2.3.1", "sass": "^1.32.12", "sass-loader": "^11.1.0", "svelte": "^3.38.2", @@ -61,7 +63,15 @@ "test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", - "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch", - "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"" + "precommit": "lint-staged", + "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch", + "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"", + "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", + "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" + }, + "lint-staged": { + "*.ts": [ + "prettier --write" + ] } } diff --git a/front/src/Api/iframe/chat.ts b/front/src/Api/iframe/chat.ts index 5f73b744..7d8e6f71 100644 --- a/front/src/Api/iframe/chat.ts +++ b/front/src/Api/iframe/chat.ts @@ -23,7 +23,7 @@ class WorkadventureChatCommands extends IframeApiContribution { diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index 8e9943b2..c7655b84 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -90,7 +90,7 @@ class WorkAdventureUiCommands extends IframeApiContribution - import {enableCameraSceneVisibilityStore, gameOverlayVisibilityStore} from "../Stores/MediaStore"; + import {enableCameraSceneVisibilityStore} from "../Stores/MediaStore"; import CameraControls from "./CameraControls.svelte"; import MyCamera from "./MyCamera.svelte"; import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte"; @@ -21,10 +21,13 @@ import AudioPlaying from "./UI/AudioPlaying.svelte"; import {soundPlayingStore} from "../Stores/SoundPlayingStore"; import ErrorDialog from "./UI/ErrorDialog.svelte"; + import VideoOverlay from "./Video/VideoOverlay.svelte"; + import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore"; import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte"; export let game: Game; +
@@ -68,6 +71,7 @@ --> {#if $gameOverlayVisibilityStore}
+
diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index 5c17a9fe..d6b31af4 100644 --- a/front/src/Components/CameraControls.svelte +++ b/front/src/Components/CameraControls.svelte @@ -7,6 +7,11 @@ import cinemaCloseImg from "./images/cinema-close.svg"; import microphoneImg from "./images/microphone.svg"; import microphoneCloseImg from "./images/microphone-close.svg"; + import layoutPresentationImg from "./images/layout-presentation.svg"; + import layoutChatImg from "./images/layout-chat.svg"; + import {layoutModeStore} from "../Stores/StreamableCollectionStore"; + import {LayoutMode} from "../WebRtc/LayoutManager"; + import {peerStore} from "../Stores/PeerStore"; function screenSharingClick(): void { if ($requestedScreenSharingState === true) { @@ -32,10 +37,24 @@ } } + function switchLayoutMode() { + if ($layoutModeStore === LayoutMode.Presentation) { + $layoutModeStore = LayoutMode.VideoChat; + } else { + $layoutModeStore = LayoutMode.Presentation; + } + }
+
+ {#if $layoutModeStore === LayoutMode.Presentation } + Switch to mosaic mode + {:else} + Switch to presentation mode + {/if} +
{#if $requestedScreenSharingState} Start screen sharing diff --git a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte index a22da2fa..79ad1810 100644 --- a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte +++ b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte @@ -58,7 +58,7 @@
- {#each [...Array(NB_BARS).keys()] as i} + {#each [...Array(NB_BARS).keys()] as i (i)}
{/each}
diff --git a/front/src/Components/SoundMeterWidget.svelte b/front/src/Components/SoundMeterWidget.svelte index 30650e3f..74d3f7b3 100644 --- a/front/src/Components/SoundMeterWidget.svelte +++ b/front/src/Components/SoundMeterWidget.svelte @@ -6,8 +6,6 @@ export let stream: MediaStream|null; let volume = 0; - const NB_BARS = 5; - let timeout: ReturnType; const soundMeter = new SoundMeter(); let display = false; @@ -23,7 +21,7 @@ timeout = setInterval(() => { try{ - volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0)); + volume = soundMeter.getVolume(); //console.log(volume); }catch(err){ @@ -45,9 +43,9 @@
- 1}> - 2}> - 3}> - 4}> 5}> + 10}> + 15}> + 40}> + 70}>
diff --git a/front/src/Components/Video/ChatLayout.svelte b/front/src/Components/Video/ChatLayout.svelte new file mode 100644 index 00000000..ac91217e --- /dev/null +++ b/front/src/Components/Video/ChatLayout.svelte @@ -0,0 +1,35 @@ + + +
+ {#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)} + + {/each} +
diff --git a/front/src/Components/Video/LocalStreamMediaBox.svelte b/front/src/Components/Video/LocalStreamMediaBox.svelte new file mode 100644 index 00000000..55c7f22c --- /dev/null +++ b/front/src/Components/Video/LocalStreamMediaBox.svelte @@ -0,0 +1,16 @@ + + + +
+ {#if stream} + + {/if} +
diff --git a/front/src/Components/Video/MediaBox.svelte b/front/src/Components/Video/MediaBox.svelte new file mode 100644 index 00000000..9160a7a9 --- /dev/null +++ b/front/src/Components/Video/MediaBox.svelte @@ -0,0 +1,20 @@ + + +
+ {#if streamable instanceof VideoPeer} + + {:else if streamable instanceof ScreenSharingPeer} + + {:else} + + {/if} +
diff --git a/front/src/Components/Video/PresentationLayout.svelte b/front/src/Components/Video/PresentationLayout.svelte new file mode 100644 index 00000000..f68dd2f1 --- /dev/null +++ b/front/src/Components/Video/PresentationLayout.svelte @@ -0,0 +1,24 @@ + + +
+ {#if $videoFocusStore } + + {/if} +
+ diff --git a/front/src/Components/Video/ScreenSharingMediaBox.svelte b/front/src/Components/Video/ScreenSharingMediaBox.svelte new file mode 100644 index 00000000..c6e1564f --- /dev/null +++ b/front/src/Components/Video/ScreenSharingMediaBox.svelte @@ -0,0 +1,33 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if $streamStore === null} + {name} + {:else} + + {/if} +
+ + diff --git a/front/src/Components/Video/VideoMediaBox.svelte b/front/src/Components/Video/VideoMediaBox.svelte new file mode 100644 index 00000000..1a581914 --- /dev/null +++ b/front/src/Components/Video/VideoMediaBox.svelte @@ -0,0 +1,48 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if !$constraintStore || $constraintStore.video === false} + {name} + {/if} + {#if $constraintStore && $constraintStore.audio === false} + Muted + {/if} + + {#if $streamStore } + + {/if} + + {#if $constraintStore && $constraintStore.audio !== false} + + {/if} +
+ diff --git a/front/src/Components/Video/VideoOverlay.svelte b/front/src/Components/Video/VideoOverlay.svelte new file mode 100644 index 00000000..feb13743 --- /dev/null +++ b/front/src/Components/Video/VideoOverlay.svelte @@ -0,0 +1,23 @@ + + +
+ {#if $layoutModeStore === LayoutMode.Presentation } + + {:else } + + {/if} +
+ + diff --git a/front/src/Components/Video/images/blockSign.svg b/front/src/Components/Video/images/blockSign.svg new file mode 100644 index 00000000..c64ba294 --- /dev/null +++ b/front/src/Components/Video/images/blockSign.svg @@ -0,0 +1,22 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/src/Components/Video/images/report.svg b/front/src/Components/Video/images/report.svg new file mode 100644 index 00000000..14753256 --- /dev/null +++ b/front/src/Components/Video/images/report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/Video/utils.ts b/front/src/Components/Video/utils.ts new file mode 100644 index 00000000..ab7a63be --- /dev/null +++ b/front/src/Components/Video/utils.ts @@ -0,0 +1,27 @@ +export function getColorByString(str: string) : string|null { + let hash = 0; + if (str.length === 0) { + return null; + } + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 255; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +} + +export function srcObject(node: HTMLVideoElement, stream: MediaStream) { + node.srcObject = stream; + return { + update(newStream: MediaStream) { + if (node.srcObject != newStream) { + node.srcObject = newStream + } + } + } +} diff --git a/front/src/Components/images/layout-chat.svg b/front/src/Components/images/layout-chat.svg new file mode 100644 index 00000000..af071d49 --- /dev/null +++ b/front/src/Components/images/layout-chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/images/layout-presentation.svg b/front/src/Components/images/layout-presentation.svg new file mode 100644 index 00000000..bf65d002 --- /dev/null +++ b/front/src/Components/images/layout-presentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 8f199c3e..1ea5e461 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -35,10 +35,10 @@ import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { jitsiFactory } from "../../WebRtc/JitsiFactory"; import { - AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, CenterListener, + AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, + Box, JITSI_MESSAGE_PROPERTIES, layoutManager, - LayoutMode, ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES, @@ -94,6 +94,9 @@ import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import AnimatedTiles from "phaser-animated-tiles"; import {soundManager} from "./SoundManager"; +import {screenSharingPeerStore} from "../../Stores/PeerStore"; +import {videoFocusStore} from "../../Stores/VideoFocusStore"; +import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore"; export interface GameSceneInitInterface { initPosition: PointInterface | null, @@ -132,7 +135,7 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; -export class GameScene extends DirtyScene implements CenterListener { +export class GameScene extends DirtyScene { Terrains: Array; CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; @@ -172,8 +175,6 @@ export class GameScene extends DirtyScene implements CenterListener { y: -1000 } - private presentationModeSprite!: Sprite; - private chatModeSprite!: Sprite; private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. @@ -277,7 +278,6 @@ export class GameScene extends DirtyScene implements CenterListener { this.onMapLoad(data); } - this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', { frameWidth: 32, frameHeight: 32 }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); //eslint-disable-next-line @typescript-eslint/no-explicit-any (this.load as any).rexWebFont({ @@ -497,10 +497,6 @@ export class GameScene extends DirtyScene implements CenterListener { this.outlinedItem?.activate(); }); - this.presentationModeSprite = new PresentationModeIcon(this, 36, this.game.renderer.height - 2); - this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); - this.chatModeSprite = new ChatModeIcon(this, 70, this.game.renderer.height - 2); - this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 2) // FIXME: change this to use the UserInputManager class for input @@ -512,7 +508,8 @@ export class GameScene extends DirtyScene implements CenterListener { this.reposition(); // From now, this game scene will be notified of reposition events - layoutManager.setListener(this); + biggestAvailableAreaStore.subscribe((box) => this.updateCameraOffset(box)); + this.triggerOnMapLayerPropertyChange(); this.listenToIframeEvents(); @@ -643,21 +640,19 @@ export class GameScene extends DirtyScene implements CenterListener { // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); peerStore.connectToSimplePeer(this.simplePeer); + screenSharingPeerStore.connectToSimplePeer(this.simplePeer); + videoFocusStore.connectToSimplePeer(this.simplePeer); this.GlobalMessageManager = new GlobalMessageManager(this.connection); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); const self = this; this.simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { - self.presentationModeSprite.setVisible(true); - self.chatModeSprite.setVisible(true); + onConnect(peer) { self.openChatIcon.setVisible(true); audioManager.decreaseVolume(); }, onDisconnect(userId: number) { if (self.simplePeer.getNbConnections() === 0) { - self.presentationModeSprite.setVisible(false); - self.chatModeSprite.setVisible(false); self.openChatIcon.setVisible(false); audioManager.restoreVolume(); } @@ -1090,23 +1085,6 @@ ${escapedMessage} this.MapPlayersByKey = new Map(); } - private switchLayoutMode(): void { - //if discussion is activated, this layout cannot be activated - if (mediaManager.activatedDiscussion) { - return; - } - const mode = layoutManager.getLayoutMode(); - if (mode === LayoutMode.Presentation) { - layoutManager.switchLayoutMode(LayoutMode.VideoChat); - this.presentationModeSprite.setFrame(1); - this.chatModeSprite.setFrame(2); - } else { - layoutManager.switchLayoutMode(LayoutMode.Presentation); - this.presentationModeSprite.setFrame(0); - this.chatModeSprite.setFrame(3); - } - } - private initStartXAndStartY() { // If there is an init position passed if (this.initPosition !== null) { @@ -1219,7 +1197,7 @@ ${escapedMessage} initCamera() { this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.cameras.main.startFollow(this.CurrentPlayer, true); - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } createCollisionWithPlayer() { @@ -1364,7 +1342,7 @@ ${escapedMessage} * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ update(time: number, delta: number): void { - mediaManager.updateScene(); + this.dirty = false; this.currentTick = time; this.CurrentPlayer.moveUser(delta); @@ -1594,20 +1572,17 @@ ${escapedMessage} } private reposition(): void { - this.presentationModeSprite.setY(this.game.renderer.height - 2); - this.chatModeSprite.setY(this.game.renderer.height - 2); this.openChatIcon.setY(this.game.renderer.height - 2); // Recompute camera offset if needed - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } /** * Updates the offset of the character compared to the center of the screen according to the layout manager * (tries to put the character in the center of the remaining space if there is a discussion going on. */ - private updateCameraOffset(): void { - const array = layoutManager.findBiggestAvailableArray(); + private updateCameraOffset(array: Box): void { const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart; const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart; @@ -1617,10 +1592,6 @@ ${escapedMessage} this.cameras.main.setFollowOffset((xCenter - game.offsetWidth / 2) * window.devicePixelRatio / this.scale.zoom, (yCenter - game.offsetHeight / 2) * window.devicePixelRatio / this.scale.zoom); } - public onCenterChange(): void { - this.updateCameraOffset(); - } - public startJitsi(roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string | undefined, 'jitsiConfig'); @@ -1680,6 +1651,6 @@ ${escapedMessage} zoomByFactor(zoomFactor: number) { waScaleManager.zoomModifier *= zoomFactor; - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } } diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index bf9c9a08..4fc58f5d 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -3,17 +3,17 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene"; import {gameManager} from "../Game/GameManager"; import {localUserStore} from "../../Connexion/LocalUserStore"; -import {mediaManager} from "../../WebRtc/MediaManager"; import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../../Url/UrlManager"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {menuIconVisible} from "../../Stores/MenuStore"; +import {videoConstraintStore} from "../../Stores/MediaStore"; +import {showReportScreenStore} from "../../Stores/ShowReportScreenStore"; import { HtmlUtils } from '../../WebRtc/HtmlUtils'; import { iframeListener } from '../../Api/IframeListener'; import { Subscription } from 'rxjs'; -import { videoConstraintStore } from "../../Stores/MediaStore"; import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent"; import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem"; import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; @@ -111,9 +111,11 @@ export class MenuScene extends Phaser.Scene { }); this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous); - mediaManager.setShowReportModalCallBacks((userId, userName) => { + showReportScreenStore.subscribe((user) => { this.closeAll(); - this.gameReportElement.open(parseInt(userId), userName); + if (user !== null) { + this.gameReportElement.open(user.userId, user.userName); + } }); this.input.keyboard.on('keyup-TAB', () => { diff --git a/front/src/Stores/BiggestAvailableAreaStore.ts b/front/src/Stores/BiggestAvailableAreaStore.ts new file mode 100644 index 00000000..716f37fc --- /dev/null +++ b/front/src/Stores/BiggestAvailableAreaStore.ts @@ -0,0 +1,127 @@ +import {get, writable} from "svelte/store"; +import type {Box} from "../WebRtc/LayoutManager"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; +import {LayoutMode} from "../WebRtc/LayoutManager"; +import {layoutModeStore} from "./StreamableCollectionStore"; + +/** + * Tries to find the biggest available box of remaining space (this is a space where we can center the character) + */ +function findBiggestAvailableArea(): Box { + const game = HtmlUtils.querySelectorOrFail('#game canvas'); + if (get(layoutModeStore) === LayoutMode.VideoChat) { + const children = document.querySelectorAll('div.chat-mode > div'); + const htmlChildren = Array.from(children.values()); + + // No chat? Let's go full center + if (htmlChildren.length === 0) { + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + + const lastDiv = htmlChildren[htmlChildren.length - 1]; + // Compute area between top right of the last div and bottom right of window + const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) + * (game.offsetHeight - lastDiv.offsetTop); + + // Compute area between bottom of last div and bottom of the screen on whole width + const area2 = game.offsetWidth + * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight)); + + if (area1 < 0 && area2 < 0) { + // If screen is full, let's not attempt something foolish and simply center character in the middle. + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + if (area1 <= area2) { + return { + xStart: 0, + yStart: lastDiv.offsetTop + lastDiv.offsetHeight, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } else { + return { + xStart: lastDiv.offsetLeft + lastDiv.offsetWidth, + yStart: lastDiv.offsetTop, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + } else { + // Possible destinations: at the center bottom or at the right bottom. + const mainSectionChildren = Array.from(document.querySelectorAll('div.main-section > div').values()); + const sidebarChildren = Array.from(document.querySelectorAll('aside.sidebar > div').values()); + + // No presentation? Let's center on the screen + if (mainSectionChildren.length === 0) { + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + + // At this point, we know we have at least one element in the main section. + const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1]; + + const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) + * (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth); + + let leftSideBar: number; + let bottomSideBar: number; + if (sidebarChildren.length === 0) { + leftSideBar = HtmlUtils.getElementByIdOrFail('sidebar').offsetLeft; + bottomSideBar = 0; + } else { + const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1]; + leftSideBar = lastSideBarChildren.offsetLeft; + bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight; + } + const sideBarArea = (game.offsetWidth - leftSideBar) + * (game.offsetHeight - bottomSideBar); + + if (presentationArea <= sideBarArea) { + return { + xStart: leftSideBar, + yStart: bottomSideBar, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } else { + return { + xStart: 0, + yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight, + xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area + yEnd: game.offsetHeight + } + } + } +} + + +/** + * A store that contains the list of (video) peers we are connected to. + */ +function createBiggestAvailableAreaStore() { + + const { subscribe, set } = writable({xStart:0, yStart: 0, xEnd: 1, yEnd: 1}); + + return { + subscribe, + recompute: () => { + set(findBiggestAvailableArea()); + } + }; +} + +export const biggestAvailableAreaStore = createBiggestAvailableAreaStore(); diff --git a/front/src/Stores/GameOverlayStoreVisibility.ts b/front/src/Stores/GameOverlayStoreVisibility.ts new file mode 100644 index 00000000..c58c929d --- /dev/null +++ b/front/src/Stores/GameOverlayStoreVisibility.ts @@ -0,0 +1,17 @@ +import {writable} from "svelte/store"; + +/** + * A store that contains whether the game overlay is shown or not. + * Typically, the overlay is hidden when entering Jitsi meet. + */ +function createGameOverlayVisibilityStore() { + const { subscribe, set, update } = writable(false); + + return { + subscribe, + showGameOverlay: () => set(true), + hideGameOverlay: () => set(false), + }; +} + +export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore(); diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index d622511e..b2cd9f42 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -1,13 +1,13 @@ import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import {peerStore} from "./PeerStore"; import {localUserStore} from "../Connexion/LocalUserStore"; -import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap"; import {userMovingStore} from "./GameStore"; import {HtmlUtils} from "../WebRtc/HtmlUtils"; import {BrowserTooOldError} from "./Errors/BrowserTooOldError"; import {errorStore} from "./ErrorStore"; import {isIOS} from "../WebRtc/DeviceUtils"; import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS"; +import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; /** * A store that contains the camera state requested by the user (on or off). @@ -50,20 +50,6 @@ export const visibilityStore = readable(document.visibilityState === 'visible', }; }); -/** - * A store that contains whether the game overlay is shown or not. - * Typically, the overlay is hidden when entering Jitsi meet. - */ -function createGameOverlayVisibilityStore() { - const { subscribe, set, update } = writable(false); - - return { - subscribe, - showGameOverlay: () => set(true), - hideGameOverlay: () => set(false), - }; -} - /** * A store that contains whether the EnableCameraScene is shown or not. */ @@ -79,7 +65,6 @@ function createEnableCameraSceneVisibilityStore() { export const requestedCameraState = createRequestedCameraState(); export const requestedMicrophoneState = createRequestedMicrophoneState(); -export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore(); export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore(); /** diff --git a/front/src/Stores/PeerStore.ts b/front/src/Stores/PeerStore.ts index a582e692..b3690595 100644 --- a/front/src/Stores/PeerStore.ts +++ b/front/src/Stores/PeerStore.ts @@ -1,26 +1,62 @@ -import { derived, writable, Writable } from "svelte/store"; -import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; -import type {SimplePeer} from "../WebRtc/SimplePeer"; +import {derived, get, readable, writable} from "svelte/store"; +import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; +import {VideoPeer} from "../WebRtc/VideoPeer"; +import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; /** - * A store that contains the camera state requested by the user (on or off). + * A store that contains the list of (video) peers we are connected to. */ function createPeerStore() { - let users = new Map(); + let peers = new Map(); - const { subscribe, set, update } = writable(users); + const { subscribe, set, update } = writable(peers); return { subscribe, connectToSimplePeer: (simplePeer: SimplePeer) => { - users = new Map(); - set(users); + peers = new Map(); + set(peers); simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { + onConnect(peer: RemotePeer) { + if (peer instanceof VideoPeer) { + update(users => { + users.set(peer.userId, peer); + return users; + }); + } + }, + onDisconnect(userId: number) { update(users => { - users.set(user.userId, user); + users.delete(userId); return users; }); + } + }) + } + }; +} + +/** + * A store that contains the list of screen sharing peers we are connected to. + */ +function createScreenSharingPeerStore() { + let peers = new Map(); + + const { subscribe, set, update } = writable(peers); + + return { + subscribe, + connectToSimplePeer: (simplePeer: SimplePeer) => { + peers = new Map(); + set(peers); + simplePeer.registerPeerConnectionListener({ + onConnect(peer: RemotePeer) { + if (peer instanceof ScreenSharingPeer) { + update(users => { + users.set(peer.userId, peer); + return users; + }); + } }, onDisconnect(userId: number) { update(users => { @@ -34,3 +70,56 @@ function createPeerStore() { } export const peerStore = createPeerStore(); +export const screenSharingPeerStore = createScreenSharingPeerStore(); + +/** + * A store that contains ScreenSharingPeer, ONLY if those ScreenSharingPeer are emitting a stream towards us! + */ +function createScreenSharingStreamStore() { + let peers = new Map(); + + return readable>(peers, function start(set) { + + let unsubscribes: (()=>void)[] = []; + + const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => { + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + unsubscribes = []; + + peers = new Map(); + + screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => { + + if (screenSharingPeer.isReceivingScreenSharingStream()) { + peers.set(key, screenSharingPeer); + } + + unsubscribes.push(screenSharingPeer.streamStore.subscribe((stream) => { + if (stream) { + peers.set(key, screenSharingPeer); + } else { + peers.delete(key); + } + set(peers); + })); + + }); + + set(peers); + + }); + + return function stop() { + unsubscribe(); + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + }; + }) +} + +export const screenSharingStreamStore = createScreenSharingStreamStore(); + + diff --git a/front/src/Stores/ScreenSharingStore.ts b/front/src/Stores/ScreenSharingStore.ts index ec5aa46f..ccb8c02c 100644 --- a/front/src/Stores/ScreenSharingStore.ts +++ b/front/src/Stores/ScreenSharingStore.ts @@ -1,16 +1,10 @@ import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import {peerStore} from "./PeerStore"; -import {localUserStore} from "../Connexion/LocalUserStore"; -import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap"; -import {userMovingStore} from "./GameStore"; -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import { - audioConstraintStore, cameraEnergySavingStore, - enableCameraSceneVisibilityStore, - gameOverlayVisibilityStore, LocalStreamStoreValue, privacyShutdownStore, - requestedCameraState, - requestedMicrophoneState, videoConstraintStore +import type { + LocalStreamStoreValue, } from "./MediaStore"; +import {DivImportance} from "../WebRtc/LayoutManager"; +import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -191,3 +185,33 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) set($peerStore.size !== 0); }); + +export interface ScreenSharingLocalMedia { + uniqueId: string; + stream: MediaStream|null; + //subscribe(this: void, run: Subscriber, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber; +} + +/** + * The representation of the screen sharing stream. + */ +export const screenSharingLocalMedia = readable(null, function start(set) { + + const localMedia: ScreenSharingLocalMedia = { + uniqueId: "localScreenSharingStream", + stream: null + } + + const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => { + if (screenSharingLocalStream.type === "success") { + localMedia.stream = screenSharingLocalStream.stream; + } else { + localMedia.stream = null; + } + set(localMedia); + }); + + return function stop() { + unsubscribe(); + }; +}) diff --git a/front/src/Stores/ShowReportScreenStore.ts b/front/src/Stores/ShowReportScreenStore.ts new file mode 100644 index 00000000..f05e545a --- /dev/null +++ b/front/src/Stores/ShowReportScreenStore.ts @@ -0,0 +1,3 @@ +import {writable} from "svelte/store"; + +export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null); diff --git a/front/src/Stores/StreamableCollectionStore.ts b/front/src/Stores/StreamableCollectionStore.ts new file mode 100644 index 00000000..bbcd354f --- /dev/null +++ b/front/src/Stores/StreamableCollectionStore.ts @@ -0,0 +1,43 @@ +import {derived, get, Readable, writable} from "svelte/store"; +import {ScreenSharingLocalMedia, screenSharingLocalMedia} from "./ScreenSharingStore"; +import { peerStore, screenSharingStreamStore} from "./PeerStore"; +import type {RemotePeer} from "../WebRtc/SimplePeer"; +import {LayoutMode} from "../WebRtc/LayoutManager"; + +export type Streamable = RemotePeer | ScreenSharingLocalMedia; + +export const layoutModeStore = writable(LayoutMode.Presentation); + +/** + * A store that contains everything that can produce a stream (so the peers + the local screen sharing stream) + */ +function createStreamableCollectionStore(): Readable> { + + return derived([ + screenSharingStreamStore, + peerStore, + screenSharingLocalMedia, + ], ([ + $screenSharingStreamStore, + $peerStore, + $screenSharingLocalMedia, + ], set) => { + + const peers = new Map(); + + const addPeer = (peer: Streamable) => { + peers.set(peer.uniqueId, peer); + }; + + $screenSharingStreamStore.forEach(addPeer); + $peerStore.forEach(addPeer); + + if ($screenSharingLocalMedia?.stream) { + addPeer($screenSharingLocalMedia); + } + + set(peers); + }); +} + +export const streamableCollectionStore = createStreamableCollectionStore(); diff --git a/front/src/Stores/VideoFocusStore.ts b/front/src/Stores/VideoFocusStore.ts new file mode 100644 index 00000000..28e948ce --- /dev/null +++ b/front/src/Stores/VideoFocusStore.ts @@ -0,0 +1,47 @@ +import {writable} from "svelte/store"; +import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; +import {VideoPeer} from "../WebRtc/VideoPeer"; +import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; +import type {Streamable} from "./StreamableCollectionStore"; + +/** + * A store that contains the peer / media that has currently the "importance" focus. + */ +function createVideoFocusStore() { + const { subscribe, set, update } = writable(null); + + let focusedMedia: Streamable | null = null; + + return { + subscribe, + focus: (media: Streamable) => { + focusedMedia = media; + set(media); + }, + removeFocus: () => { + focusedMedia = null; + set(null); + }, + toggleFocus: (media: Streamable) => { + if (media !== focusedMedia) { + focusedMedia = media; + } else { + focusedMedia = null; + } + set(focusedMedia); + }, + connectToSimplePeer: (simplePeer: SimplePeer) => { + simplePeer.registerPeerConnectionListener({ + onConnect(peer: RemotePeer) { + }, + onDisconnect(userId: number) { + if ((focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) && focusedMedia.userId === userId) { + set(null); + } + } + }) + } + }; +} + +export const videoFocusStore = createVideoFocusStore(); diff --git a/front/src/WebRtc/DiscussionManager.ts b/front/src/WebRtc/DiscussionManager.ts index 504ee91b..b728eca0 100644 --- a/front/src/WebRtc/DiscussionManager.ts +++ b/front/src/WebRtc/DiscussionManager.ts @@ -1,9 +1,9 @@ import {HtmlUtils} from "./HtmlUtils"; -import type {ShowReportCallBack} from "./MediaManager"; import type {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {connectionManager} from "../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../Url/UrlManager"; import {iframeListener} from "../Api/IframeListener"; +import {showReportScreenStore} from "../Stores/ShowReportScreenStore"; export type SendMessageCallback = (message:string) => void; @@ -104,11 +104,10 @@ export class DiscussionManager { } public addParticipant( - userId: number|string, + userId: number|'me', name: string|undefined, img?: string|undefined, isMe: boolean = false, - showReportCallBack?: ShowReportCallBack ) { const divParticipant: HTMLDivElement = document.createElement('div'); divParticipant.classList.add('participant'); @@ -132,16 +131,13 @@ export class DiscussionManager { !isMe && connectionManager.getConnexionType && connectionManager.getConnexionType !== GameConnexionTypes.anonymous + && userId !== 'me' ) { const reportBanUserAction: HTMLButtonElement = document.createElement('button'); reportBanUserAction.classList.add('report-btn') reportBanUserAction.innerText = 'Report'; reportBanUserAction.addEventListener('click', () => { - if(showReportCallBack) { - showReportCallBack(`${userId}`, name); - }else{ - console.info('report feature is not activated!'); - } + showReportScreenStore.set({ userId: userId, userName: name ? name : ''}); }); divParticipant.appendChild(reportBanUserAction); } diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 3d92baac..da295a98 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -15,14 +15,6 @@ export enum DivImportance { Normal = "Normal", } -/** - * Classes implementing this interface can be notified when the center of the screen (the player position) should be - * changed. - */ -export interface CenterListener { - onCenterChange(): void; -} - export const ON_ACTION_TRIGGER_BUTTON = 'onaction'; export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger'; @@ -34,293 +26,12 @@ export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage'; export const AUDIO_VOLUME_PROPERTY = 'audioVolume'; export const AUDIO_LOOP_PROPERTY = 'audioLoop'; -/** - * This class is in charge of the video-conference layout. - * It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode. - */ +export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number}; + class LayoutManager { - private mode: LayoutMode = LayoutMode.Presentation; - - private importantDivs: Map = new Map(); - private normalDivs: Map = new Map(); - private listener: CenterListener|null = null; - private actionButtonTrigger: Map = new Map(); private actionButtonInformation: Map = new Map(); - public setListener(centerListener: CenterListener|null) { - this.listener = centerListener; - } - - public add(importance: DivImportance, userId: string, html: string): void { - const div = document.createElement('div'); - div.innerHTML = html; - div.id = "user-"+userId; - div.className = "media-container" - div.onclick = () => { - const parentId = div.parentElement?.id; - if (parentId === 'sidebar' || parentId === 'chat-mode') { - this.focusOn(userId); - } else { - this.removeFocusOn(userId); - } - } - - if (importance === DivImportance.Important) { - this.importantDivs.set(userId, div); - - // If this is the first video with high importance, let's switch mode automatically. - if (this.importantDivs.size === 1 && this.mode === LayoutMode.VideoChat) { - this.switchLayoutMode(LayoutMode.Presentation); - } - } else if (importance === DivImportance.Normal) { - this.normalDivs.set(userId, div); - } else { - throw new Error('Unexpected importance'); - } - - this.positionDiv(div, importance); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - } - - private positionDiv(elem: HTMLDivElement, importance: DivImportance): void { - if (this.mode === LayoutMode.VideoChat) { - const chatModeDiv = HtmlUtils.getElementByIdOrFail('chat-mode'); - chatModeDiv.appendChild(elem); - } else { - if (importance === DivImportance.Important) { - const mainSectionDiv = HtmlUtils.getElementByIdOrFail('main-section'); - mainSectionDiv.appendChild(elem); - } else if (importance === DivImportance.Normal) { - const sideBarDiv = HtmlUtils.getElementByIdOrFail('sidebar'); - sideBarDiv.appendChild(elem); - } - } - } - - /** - * Put the screen in presentation mode and move elem in presentation mode (and all other videos in normal mode) - */ - private focusOn(userId: string): void { - const focusedDiv = this.getDivByUserId(userId); - for (const [importantUserId, importantDiv] of this.importantDivs.entries()) { - //this.positionDiv(importantDiv, DivImportance.Normal); - this.importantDivs.delete(importantUserId); - this.normalDivs.set(importantUserId, importantDiv); - } - this.normalDivs.delete(userId); - this.importantDivs.set(userId, focusedDiv); - //this.positionDiv(focusedDiv, DivImportance.Important); - this.switchLayoutMode(LayoutMode.Presentation); - } - - /** - * Removes userId from presentation mode - */ - private removeFocusOn(userId: string): void { - const importantDiv = this.importantDivs.get(userId); - if (importantDiv === undefined) { - throw new Error('Div with user id "'+userId+'" is not in important mode'); - } - this.normalDivs.set(userId, importantDiv); - this.importantDivs.delete(userId); - - this.positionDiv(importantDiv, DivImportance.Normal); - } - - private getDivByUserId(userId: string): HTMLDivElement { - let div = this.importantDivs.get(userId); - if (div !== undefined) { - return div; - } - div = this.normalDivs.get(userId); - if (div !== undefined) { - return div; - } - throw new Error('Could not find media with user id '+userId); - } - - /** - * Removes the DIV matching userId. - */ - public remove(userId: string): void { - console.log('Removing video for userID '+userId+'.'); - let div = this.importantDivs.get(userId); - if (div !== undefined) { - div.remove(); - this.importantDivs.delete(userId); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - return; - } - - div = this.normalDivs.get(userId); - if (div !== undefined) { - div.remove(); - this.normalDivs.delete(userId); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - return; - } - - console.log('Cannot remove userID '+userId+'. Already removed?'); - //throw new Error('Could not find user ID "'+userId+'"'); - } - - private adjustVideoChatClass(): void { - const chatModeDiv = HtmlUtils.getElementByIdOrFail('chat-mode'); - chatModeDiv.classList.remove('one-col', 'two-col', 'three-col', 'four-col'); - - const nbUsers = this.importantDivs.size + this.normalDivs.size; - - if (nbUsers <= 1) { - chatModeDiv.classList.add('one-col'); - } else if (nbUsers <= 4) { - chatModeDiv.classList.add('two-col'); - } else if (nbUsers <= 9) { - chatModeDiv.classList.add('three-col'); - } else { - chatModeDiv.classList.add('four-col'); - } - } - - public switchLayoutMode(layoutMode: LayoutMode) { - this.mode = layoutMode; - - if (layoutMode === LayoutMode.Presentation) { - HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'flex'; - HtmlUtils.getElementByIdOrFail('main-section').style.display = 'flex'; - HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'none'; - } else { - HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'none'; - HtmlUtils.getElementByIdOrFail('main-section').style.display = 'none'; - HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'grid'; - } - - for (const div of this.importantDivs.values()) { - this.positionDiv(div, DivImportance.Important); - } - for (const div of this.normalDivs.values()) { - this.positionDiv(div, DivImportance.Normal); - } - this.listener?.onCenterChange(); - } - - public getLayoutMode(): LayoutMode { - return this.mode; - } - - /*public getGameCenter(): {x: number, y: number} { - - }*/ - - /** - * Tries to find the biggest available box of remaining space (this is a space where we can center the character) - */ - public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} { - const game = HtmlUtils.querySelectorOrFail('#game canvas'); - if (this.mode === LayoutMode.VideoChat) { - const children = document.querySelectorAll('div.chat-mode > div'); - const htmlChildren = Array.from(children.values()); - - // No chat? Let's go full center - if (htmlChildren.length === 0) { - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - - const lastDiv = htmlChildren[htmlChildren.length - 1]; - // Compute area between top right of the last div and bottom right of window - const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) - * (game.offsetHeight - lastDiv.offsetTop); - - // Compute area between bottom of last div and bottom of the screen on whole width - const area2 = game.offsetWidth - * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight)); - - if (area1 < 0 && area2 < 0) { - // If screen is full, let's not attempt something foolish and simply center character in the middle. - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - if (area1 <= area2) { - console.log('lastDiv', lastDiv.offsetTop, lastDiv.offsetHeight); - return { - xStart: 0, - yStart: lastDiv.offsetTop + lastDiv.offsetHeight, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } else { - console.log('lastDiv', lastDiv.offsetTop); - return { - xStart: lastDiv.offsetLeft + lastDiv.offsetWidth, - yStart: lastDiv.offsetTop, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - } else { - // Possible destinations: at the center bottom or at the right bottom. - const mainSectionChildren = Array.from(document.querySelectorAll('div.main-section > div').values()); - const sidebarChildren = Array.from(document.querySelectorAll('aside.sidebar > div').values()); - - // No presentation? Let's center on the screen - if (mainSectionChildren.length === 0) { - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - - // At this point, we know we have at least one element in the main section. - const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1]; - - const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) - * (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth); - - let leftSideBar: number; - let bottomSideBar: number; - if (sidebarChildren.length === 0) { - leftSideBar = HtmlUtils.getElementByIdOrFail('sidebar').offsetLeft; - bottomSideBar = 0; - } else { - const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1]; - leftSideBar = lastSideBarChildren.offsetLeft; - bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight; - } - const sideBarArea = (game.offsetWidth - leftSideBar) - * (game.offsetHeight - bottomSideBar); - - if (presentationArea <= sideBarArea) { - return { - xStart: leftSideBar, - yStart: bottomSideBar, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } else { - return { - xStart: 0, - yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight, - xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area - yEnd: game.offsetHeight - } - } - } - } - public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){ //delete previous element this.removeActionButton(id, userInputManager); diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 0d8a05f2..faa5edf7 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -7,7 +7,7 @@ import type { UserSimplePeerInterface } from "./SimplePeer"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable"; import { - gameOverlayVisibilityStore, localStreamStore, + localStreamStore, } from "../Stores/MediaStore"; import { screenSharingLocalStreamStore @@ -17,20 +17,13 @@ import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore" export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void; export type StartScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void; -export type ReportCallback = (message: string) => void; -export type ShowReportCallBack = (userId: string, userName: string | undefined) => void; -export type HelpCameraSettingsCallBack = () => void; import {cowebsiteCloseButtonId} from "./CoWebsiteManager"; +import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; export class MediaManager { - private remoteVideo: Map = new Map(); - //FIX ME SOUNDMETER: check stalability of sound meter calculation - //mySoundMeterElement: HTMLDivElement; - - startScreenSharingCallBacks: Set = new Set(); - stopScreenSharingCallBacks: Set = new Set(); - showReportModalCallBacks: Set = new Set(); + startScreenSharingCallBacks : Set = new Set(); + stopScreenSharingCallBacks : Set = new Set(); @@ -40,21 +33,8 @@ export class MediaManager { private userInputManager?: UserInputManager; - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*private mySoundMeter?: SoundMeter|null; - private soundMeters: Map = new Map(); - private soundMeterElements: Map = new Map();*/ - constructor() { - this.pingCameraStatus(); - - //FIX ME SOUNDMETER: check stability of sound meter calculation - /*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter')); - this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => { - this.mySoundMeterElement.children.item(index)?.classList.remove('active'); - });*/ - //Check of ask notification navigator permission this.getNotification(); @@ -68,7 +48,6 @@ export class MediaManager { } }); - let isScreenSharing = false; screenSharingLocalStreamStore.subscribe((result) => { if (result.type === 'error') { console.error(result.error); @@ -77,32 +56,7 @@ export class MediaManager { }, this.userInputManager); return; } - - if (result.stream !== null) { - isScreenSharing = true; - this.addScreenSharingActiveVideo('me', DivImportance.Normal); - HtmlUtils.getElementByIdOrFail('screen-sharing-me').srcObject = result.stream; - } else { - if (isScreenSharing) { - isScreenSharing = false; - this.removeActiveScreenSharingVideo('me'); - } - } - }); - - /*screenSharingAvailableStore.subscribe((available) => { - if (available) { - document.querySelector('.btn-monitor')?.classList.remove('hide'); - } else { - document.querySelector('.btn-monitor')?.classList.add('hide'); - } - });*/ - } - - public updateScene(){ - //FIX ME SOUNDMETER: check stability of sound meter calculation - //this.updateSoudMeter(); } public showGameOverlay(): void { @@ -137,71 +91,6 @@ export class MediaManager { gameOverlayVisibilityStore.hideGameOverlay(); } - - - addActiveVideo(user: UserSimplePeerInterface, userName: string = "") { - - const userId = '' + user.userId - - userName = userName.toUpperCase(); - const color = this.getColorByString(userName); - - const html = ` -
-
- - ${userName} - - - - -
- - - - - -
-
- `; - - layoutManager.add(DivImportance.Normal, userId, html); - - this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); - - //permit to create participant in discussion part - const showReportUser = () => { - for (const callBack of this.showReportModalCallBacks) { - callBack(userId, userName); - } - }; - this.addNewParticipant(userId, userName, undefined, showReportUser); - - const reportBanUserActionEl: HTMLImageElement = HtmlUtils.getElementByIdOrFail(`report-${userId}`); - reportBanUserActionEl.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - showReportUser(); - }); - } - - addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ - - userId = this.getScreenSharingId(userId); - const html = ` -
- -
- `; - - layoutManager.add(divImportance, userId, html); - - this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); - } - private getScreenSharingId(userId: string): string { return `screen-sharing-${userId}`; } @@ -248,61 +137,6 @@ export class MediaManager { const blockLogoElement = HtmlUtils.getElementByIdOrFail('blocking-' + userId); show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active'); } - addStreamRemoteVideo(userId: string, stream: MediaStream): void { - const remoteVideo = this.remoteVideo.get(userId); - if (remoteVideo === undefined) { - throw `Unable to find video for ${userId}`; - } - remoteVideo.srcObject = stream; - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - //sound metter - /*const soundMeter = new SoundMeter(); - soundMeter.connectToSource(stream, new AudioContext()); - this.soundMeters.set(userId, soundMeter); - this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail('soundMeter-'+userId));*/ - } - addStreamRemoteScreenSharing(userId: string, stream: MediaStream) { - // In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet - const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId)); - if (remoteVideo === undefined) { - this.addScreenSharingActiveVideo(userId); - } - - this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream); - } - - removeActiveVideo(userId: string) { - layoutManager.remove(userId); - this.remoteVideo.delete(userId); - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*this.soundMeters.get(userId)?.stop(); - this.soundMeters.delete(userId); - this.soundMeterElements.delete(userId);*/ - - //permit to remove user in discussion part - this.removeParticipant(userId); - } - removeActiveScreenSharingVideo(userId: string) { - this.removeActiveVideo(this.getScreenSharingId(userId)) - } - - isConnecting(userId: string): void { - const connectingSpinnerDiv = this.getSpinner(userId); - if (connectingSpinnerDiv === null) { - return; - } - connectingSpinnerDiv.style.display = 'block'; - } - - isConnected(userId: string): void { - const connectingSpinnerDiv = this.getSpinner(userId); - if (connectingSpinnerDiv === null) { - return; - } - connectingSpinnerDiv.style.display = 'none'; - } isError(userId: string): void { console.info("isError", `div-${userId}`); @@ -326,33 +160,11 @@ export class MediaManager { if (!element) { return null; } - const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement | null; - return connnectingSpinnerDiv; + const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; + return connectingSpinnerDiv; } - private getColorByString(str: String): String | null { - let hash = 0; - if (str.length === 0) return null; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - hash = hash & hash; - } - let color = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 255; - color += ('00' + value.toString(16)).substr(-2); - } - return color; - } - - public addNewParticipant(userId: number | string, name: string | undefined, img?: string, showReportUserCallBack?: ShowReportCallBack) { - discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack); - } - - public removeParticipant(userId: number | string) { - discussionManager.removeParticipant(userId); - } - public addTriggerCloseJitsiFrameButton(id: String, Function: Function) { + public addTriggerCloseJitsiFrameButton(id: String, Function: Function){ this.triggerCloseJistiFrame.set(id, Function); } @@ -365,16 +177,6 @@ export class MediaManager { callback(); } } - /** - * For some reasons, the microphone muted icon or the stream is not always up to date. - * Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser) - **/ - private pingCameraStatus() { - /*setInterval(() => { - console.log('ping camera status'); - this.triggerUpdatedLocalStreamCallbacks(this.localStream); - }, 30000);*/ - } public addNewMessage(name: string, message: string, isMe: boolean = false) { discussionManager.addMessage(name, message, isMe); @@ -389,61 +191,11 @@ export class MediaManager { discussionManager.onSendMessageCallback(userId, callback); } - get activatedDiscussion() { - return discussionManager.activatedDiscussion; - } - - public setUserInputManager(userInputManager: UserInputManager) { + public setUserInputManager(userInputManager : UserInputManager){ this.userInputManager = userInputManager; discussionManager.setUserInputManager(userInputManager); } - public setShowReportModalCallBacks(callback: ShowReportCallBack) { - this.showReportModalCallBacks.add(callback); - } - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*updateSoudMeter(){ - try{ - const volume = parseInt(((this.mySoundMeter ? this.mySoundMeter.getVolume() : 0) / 10).toFixed(0)); - this.setVolumeSoundMeter(volume, this.mySoundMeterElement); - - for(const indexUserId of this.soundMeters.keys()){ - const soundMeter = this.soundMeters.get(indexUserId); - const soundMeterElement = this.soundMeterElements.get(indexUserId); - if (!soundMeter || !soundMeterElement) { - return; - } - const volumeByUser = parseInt((soundMeter.getVolume() / 10).toFixed(0)); - this.setVolumeSoundMeter(volumeByUser, soundMeterElement); - } - } catch (err) { - //console.error(err); - } - }*/ - - private setVolumeSoundMeter(volume: number, element: HTMLDivElement) { - if (volume <= 0 && !element.classList.contains('active')) { - return; - } - element.classList.remove('active'); - if (volume <= 0) { - return; - } - element.classList.add('active'); - element.childNodes.forEach((value: ChildNode, index) => { - const elementChildre = element.children.item(index); - if (!elementChildre) { - return; - } - elementChildre.classList.remove('active'); - if ((index + 1) > volume) { - return; - } - elementChildre.classList.add('active'); - }); - } - public getNotification(){ //Get notification if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") { diff --git a/front/src/WebRtc/ScreenSharingPeer.ts b/front/src/WebRtc/ScreenSharingPeer.ts index d797f59b..be534276 100644 --- a/front/src/WebRtc/ScreenSharingPeer.ts +++ b/front/src/WebRtc/ScreenSharingPeer.ts @@ -1,9 +1,11 @@ import type * as SimplePeerNamespace from "simple-peer"; import {mediaManager} from "./MediaManager"; -import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable"; +import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable"; import type {RoomConnection} from "../Connexion/RoomConnection"; -import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer"; +import {MESSAGE_TYPE_CONSTRAINT, PeerStatus} from "./VideoPeer"; import type {UserSimplePeerInterface} from "./SimplePeer"; +import {Readable, readable, writable, Writable} from "svelte/store"; +import {videoFocusStore} from "../Stores/VideoFocusStore"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); @@ -17,9 +19,12 @@ export class ScreenSharingPeer extends Peer { private isReceivingStream:boolean = false; public toClose: boolean = false; public _connected: boolean = false; - private userId: number; + public readonly userId: number; + public readonly uniqueId: string; + public readonly streamStore: Readable; + public readonly statusStore: Readable; - constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, stream: MediaStream | null) { + constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) { super({ initiator: initiator ? initiator : false, //reconnectTimer: 10000, @@ -38,6 +43,55 @@ export class ScreenSharingPeer extends Peer { }); this.userId = user.userId; + this.uniqueId = 'screensharing_'+this.userId; + + this.streamStore = readable(null, (set) => { + const onStream = (stream: MediaStream|null) => { + videoFocusStore.focus(this); + set(stream); + }; + const onData = (chunk: Buffer) => { + // We unfortunately need to rely on an event to let the other party know a stream has stopped. + // It seems there is no native way to detect that. + // TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event + const message = JSON.parse(chunk.toString('utf8')); + if (message.streamEnded !== true) { + console.error('Unexpected message on screen sharing peer connection'); + return; + } + set(null); + } + + this.on('stream', onStream); + this.on('data', onData); + + return () => { + this.off('stream', onStream); + this.off('data', onData); + }; + }); + + this.statusStore = readable("connecting", (set) => { + const onConnect = () => { + set('connected'); + }; + const onError = () => { + set('error'); + }; + const onClose = () => { + set('closed'); + }; + + this.on('connect', onConnect); + this.on('error', onError); + this.on('close', onClose); + + return () => { + this.off('connect', onConnect); + this.off('error', onError); + this.off('close', onClose); + }; + }); //start listen signal for the peer connection this.on('signal', (data: unknown) => { @@ -54,27 +108,13 @@ export class ScreenSharingPeer extends Peer { this.destroy(); }); - this.on('data', (chunk: Buffer) => { - // We unfortunately need to rely on an event to let the other party know a stream has stopped. - // It seems there is no native way to detect that. - const message = JSON.parse(chunk.toString('utf8')); - if (message.streamEnded !== true) { - console.error('Unexpected message on screen sharing peer connection'); - return; - } - mediaManager.removeActiveScreenSharingVideo("" + this.userId); - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any this.on('error', (err: any) => { console.error(`screen sharing error => ${this.userId} => ${err.code}`, err); - //mediaManager.isErrorScreenSharing(this.userId); }); this.on('connect', () => { this._connected = true; - // FIXME: we need to put the loader on the screen sharing connection - mediaManager.isConnected("" + this.userId); console.info(`connect => ${this.userId}`); }); @@ -88,7 +128,6 @@ export class ScreenSharingPeer extends Peer { } private sendWebrtcScreenSharingSignal(data: unknown) { - //console.log("sendWebrtcScreenSharingSignal", data); try { this.connection.sendWebrtcScreenSharingSignal(data, this.userId); }catch (e) { @@ -100,13 +139,9 @@ export class ScreenSharingPeer extends Peer { * Sends received stream to screen. */ private stream(stream?: MediaStream) { - //console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream); - //console.log(`stream => ${this.userId} => `, stream); if(!stream){ - mediaManager.removeActiveScreenSharingVideo("" + this.userId); this.isReceivingStream = false; } else { - mediaManager.addStreamRemoteScreenSharing("" + this.userId, stream); this.isReceivingStream = true; } } @@ -121,7 +156,6 @@ export class ScreenSharingPeer extends Peer { if(!this.toClose){ return; } - mediaManager.removeActiveScreenSharingVideo("" + this.userId); // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. //console.log('Closing connection with '+userId); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 2a502bab..ecc0e21b 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -6,19 +6,15 @@ import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback, - UpdatedLocalStreamCallback } from "./MediaManager"; import {ScreenSharingPeer} from "./ScreenSharingPeer"; import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import type {RoomConnection} from "../Connexion/RoomConnection"; -import {connectionManager} from "../Connexion/ConnectionManager"; -import {GameConnexionTypes} from "../Url/UrlManager"; import {blackListManager} from "./BlackListManager"; import {get} from "svelte/store"; import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore"; import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore"; -import {DivImportance, layoutManager} from "./LayoutManager"; -import {HtmlUtils} from "./HtmlUtils"; +import {discussionManager} from "./DiscussionManager"; export interface UserSimplePeerInterface{ userId: number; @@ -28,8 +24,10 @@ export interface UserSimplePeerInterface{ webRtcPassword?: string|undefined; } +export type RemotePeer = VideoPeer | ScreenSharingPeer; + export interface PeerConnectionListener { - onConnect(user: UserSimplePeerInterface): void; + onConnect(user: RemotePeer): void; onDisconnect(userId: number): void; } @@ -124,7 +122,6 @@ export class SimplePeer { // This would be symmetrical to the way we handle disconnection. //start connection - //console.log('receiveWebrtcStart. Initiator: ', user.initiator) if(!user.initiator){ return; } @@ -159,20 +156,15 @@ export class SimplePeer { let name = user.name; if (!name) { - const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === user.userId); - if (userSearch) { - name = userSearch.name; - } + name = this.getName(user.userId); } - mediaManager.removeActiveVideo("" + user.userId); - - mediaManager.addActiveVideo(user, name); + discussionManager.removeParticipant(user.userId); this.lastWebrtcUserName = user.webRtcUser; this.lastWebrtcPassword = user.webRtcPassword; - const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection, localStream); + const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream); //permit to send message mediaManager.addSendMessageCallback(user.userId,(message: string) => { @@ -196,11 +188,20 @@ export class SimplePeer { this.PeerConnectionArray.set(user.userId, peer); for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onConnect(user); + peerConnectionListener.onConnect(peer); } return peer; } + private getName(userId: number): string { + const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId); + if (userSearch) { + return userSearch.name || ''; + } else { + return ''; + } + } + /** * create peer connection to bind users */ @@ -221,23 +222,19 @@ export class SimplePeer { return null; } - // We should display the screen sharing ONLY if we are not initiator - if (!user.initiator) { - mediaManager.removeActiveScreenSharingVideo("" + user.userId); - mediaManager.addScreenSharingActiveVideo("" + user.userId); - } - // Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing) if (user.webRtcUser === undefined) { user.webRtcUser = this.lastWebrtcUserName; user.webRtcPassword = this.lastWebrtcPassword; } - const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection, stream); + const name = this.getName(user.userId); + + const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream); this.PeerScreenSharingConnectionArray.set(user.userId, peer); for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onConnect(user); + peerConnectionListener.onConnect(peer); } return peer; } @@ -288,7 +285,7 @@ export class SimplePeer { */ private closeScreenSharingConnection(userId : number) { try { - mediaManager.removeActiveScreenSharingVideo("" + userId); + //mediaManager.removeActiveScreenSharingVideo("" + userId); const peer = this.PeerScreenSharingConnectionArray.get(userId); if (peer === undefined) { console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user") diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index 5ca8952c..0840d6ff 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -5,11 +5,15 @@ import type {RoomConnection} from "../Connexion/RoomConnection"; import {blackListManager} from "./BlackListManager"; import type {Subscription} from "rxjs"; import type {UserSimplePeerInterface} from "./SimplePeer"; -import {get} from "svelte/store"; +import {get, readable, Readable} from "svelte/store"; import {obtainedMediaConstraintStore} from "../Stores/MediaStore"; +import {DivImportance} from "./LayoutManager"; +import {discussionManager} from "./DiscussionManager"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); +export type PeerStatus = "connecting" | "connected" | "error" | "closed"; + export const MESSAGE_TYPE_CONSTRAINT = 'constraint'; export const MESSAGE_TYPE_MESSAGE = 'message'; export const MESSAGE_TYPE_BLOCKED = 'blocked'; @@ -22,12 +26,15 @@ export class VideoPeer extends Peer { public _connected: boolean = false; private remoteStream!: MediaStream; private blocked: boolean = false; - private userId: number; - private userName: string; + public readonly userId: number; + public readonly uniqueId: string; private onBlockSubscribe: Subscription; private onUnBlockSubscribe: Subscription; + public readonly streamStore: Readable; + public readonly statusStore: Readable; + public readonly constraintsStore: Readable; - constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, localStream: MediaStream | null) { + constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) { super({ initiator: initiator ? initiator : false, //reconnectTimer: 10000, @@ -46,7 +53,68 @@ export class VideoPeer extends Peer { }); this.userId = user.userId; - this.userName = user.name || ''; + this.uniqueId = 'video_'+this.userId; + + this.streamStore = readable(null, (set) => { + const onStream = (stream: MediaStream|null) => { + set(stream); + }; + const onData = (chunk: Buffer) => { + this.on('data', (chunk: Buffer) => { + const message = JSON.parse(chunk.toString('utf8')); + if (message.type === MESSAGE_TYPE_CONSTRAINT) { + if (!message.video) { + set(null); + } + } + }); + } + + this.on('stream', onStream); + this.on('data', onData); + + return () => { + this.off('stream', onStream); + this.off('data', onData); + }; + }); + + this.constraintsStore = readable(null, (set) => { + const onData = (chunk: Buffer) => { + const message = JSON.parse(chunk.toString('utf8')); + if(message.type === MESSAGE_TYPE_CONSTRAINT) { + set(message); + } + } + + this.on('data', onData); + + return () => { + this.off('data', onData); + }; + }); + + this.statusStore = readable("connecting", (set) => { + const onConnect = () => { + set('connected'); + }; + const onError = () => { + set('error'); + }; + const onClose = () => { + set('closed'); + }; + + this.on('connect', onConnect); + this.on('error', onError); + this.on('close', onClose); + + return () => { + this.off('connect', onConnect); + this.off('error', onError); + this.off('close', onClose); + }; + }); //start listen signal for the peer connection this.on('signal', (data: unknown) => { @@ -69,8 +137,6 @@ export class VideoPeer extends Peer { this.on('connect', () => { this._connected = true; - mediaManager.isConnected("" + this.userId); - console.info(`connect => ${this.userId}`); }); this.on('data', (chunk: Buffer) => { @@ -152,7 +218,6 @@ export class VideoPeer extends Peer { if (blackListManager.isBlackListed(this.userId) || this.blocked) { this.toggleRemoteStream(false); } - mediaManager.addStreamRemoteVideo("" + this.userId, stream); }catch (err){ console.error(err); } @@ -169,7 +234,7 @@ export class VideoPeer extends Peer { } this.onBlockSubscribe.unsubscribe(); this.onUnBlockSubscribe.unsubscribe(); - mediaManager.removeActiveVideo("" + this.userId); + discussionManager.removeParticipant(this.userId); // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. super.destroy(error); diff --git a/front/style/style.scss b/front/style/style.scss index 5c958309..eb34287a 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -35,102 +35,109 @@ body .message-info.info{ body .message-info.warning{ background: #ffa500d6; } -.video-container{ + +.video-container { position: relative; transition: all 0.2s ease; background-color: #00000099; cursor: url('./images/cursor_pointer.png'), pointer; -} -.video-container i{ - position: absolute; - width: 100px; - height: 100px; - left: calc(50% - 50px); - top: calc(50% - 50px); - background-color: black; - border-radius: 50%; - text-align: center; - padding-top: 32px; - font-size: 28px; - color: white; - overflow: hidden; -} -.video-container img{ - position: absolute; - display: none; - width: 40px; - height: 40px; - left: 5px; - bottom: 5px; - padding: 10px; - z-index: 2; -} -.video-container img.block-logo { - left: 30%; - bottom: 15%; - width: 150px; - height: 150px; -} + video { + width: 100%; + height: 100%; + max-height: 90vh; + cursor: url('./images/cursor_pointer.png'), pointer; + } -.video-container button.report{ - display: block; - cursor: url('./images/cursor_pointer.png'), pointer; - background: none; - background-color: rgba(0, 0, 0, 0); - border: none; - background-color: black; - border-radius: 15px; - position: absolute; - width: 0px; - height: 35px; - right: 5px; - bottom: 5px; - padding: 0px; - overflow: hidden; - z-index: 2; - transition: all .5s ease; -} + i { + position: absolute; + width: 100px; + height: 100px; + left: calc(50% - 50px); + top: calc(50% - 50px); + background-color: black; + border-radius: 50%; + text-align: center; + padding-top: 32px; + font-size: 28px; + color: white; + overflow: hidden; + } -.video-container:hover button.report{ - width: 35px; - padding: 10px; -} + img { + position: absolute; + display: none; + width: 40px; + height: 40px; + left: 5px; + bottom: 5px; + padding: 10px; + z-index: 2; + } -.video-container button.report:hover { - width: 160px; -} + img.block-logo { + left: 30%; + bottom: 15%; + width: 150px; + height: 150px; + } -.video-container button.report img{ - position: absolute; - display: block; - bottom: 5px; - left: 5px; - margin: 0; - padding: 0; - cursor: url('./images/cursor_pointer.png'), pointer; - width: 25px; - height: 25px; -} -.video-container button.report span{ - position: absolute; - bottom: 6px; - left: 36px; - color: white; - font-size: 16px; - cursor: url('./images/cursor_pointer.png'), pointer; -} -.video-container img.active { - display: block !important; -} + button.report{ + display: block; + cursor: url('./images/cursor_pointer.png'), pointer; + background: none; + background-color: rgba(0, 0, 0, 0); + border: none; + background-color: black; + border-radius: 15px; + position: absolute; + width: 0px; + height: 35px; + right: 5px; + bottom: 5px; + padding: 0px; + overflow: hidden; + z-index: 2; + transition: all .5s ease; -.video-container video{ - height: 100%; - cursor: url('./images/cursor_pointer.png'), pointer; -} + img{ + position: absolute; + display: block; + bottom: 5px; + left: 5px; + margin: 0; + padding: 0; + cursor: url('./images/cursor_pointer.png'), pointer; + width: 25px; + height: 25px; + } -.video-container video:focus{ - outline: none; + span { + position: absolute; + bottom: 6px; + left: 36px; + color: white; + font-size: 16px; + cursor: url('./images/cursor_pointer.png'), pointer; + } + + img.active { + display: block !important; + } + } + + &:hover button.report{ + width: 35px; + padding: 10px; + + &:hover { + width: 160px; + } + } + + video:focus{ + outline: none; + } } .video-container.div-myCamVideo{ @@ -204,7 +211,7 @@ video.myCamVideo{ display: inline-flex; bottom: 10px; right: 15px; - width: 180px; + width: 240px; height: 40px; text-align: center; align-content: center; @@ -224,7 +231,7 @@ video.myCamVideo{ background: #666; box-shadow: 2px 2px 24px #444; border-radius: 48px; - transform: translateY(20px); + transform: translateY(15px); transition-timing-function: ease-in-out; margin: 0 4%; } @@ -263,6 +270,16 @@ video.myCamVideo{ .btn-cam-action:hover .btn-monitor.hide{ transform: translateY(60px); } +.btn-layout{ + pointer-events: auto; + transition: all .15s; +} +.btn-layout.hide { + transform: translateY(60px); +} +.btn-cam-action:hover .btn-layout.hide{ + transform: translateY(60px); +} .btn-copy{ pointer-events: auto; transition: all .3s; diff --git a/front/webpack.config.ts b/front/webpack.config.ts index 3a69b74a..3bfc0a48 100644 --- a/front/webpack.config.ts +++ b/front/webpack.config.ts @@ -95,6 +95,7 @@ module.exports = { if (warning.code === 'a11y-no-onchange') { return } if (warning.code === 'a11y-autofocus') { return } + if (warning.code === 'a11y-media-has-caption') { return } // process as usual handleWarning(warning); diff --git a/front/yarn.lock b/front/yarn.lock index f0159242..fec87661 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -569,6 +569,14 @@ after@0.8.2: resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -609,6 +617,13 @@ ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -1121,7 +1136,7 @@ chalk@^2.0.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== @@ -1208,6 +1223,26 @@ clean-css@^4.2.3: dependencies: source-map "~0.6.0" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -1278,7 +1313,7 @@ commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@^7.0.0: +commander@^7.0.0, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== @@ -1381,6 +1416,17 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -1536,6 +1582,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + deep-equal@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -1823,7 +1874,7 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5: +enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -2426,6 +2477,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -2822,6 +2878,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -3060,6 +3121,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-path-cwd@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" @@ -3099,6 +3165,11 @@ is-regex@^1.0.4, is-regex@^1.1.2: call-bind "^1.0.2" has-symbols "^1.0.2" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3132,6 +3203,11 @@ is-typed-array@^1.1.3: foreach "^2.0.5" has-symbols "^1.0.1" +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -3309,6 +3385,40 @@ linked-list-typescript@^1.0.11: resolved "https://registry.yarnpkg.com/linked-list-typescript/-/linked-list-typescript-1.0.15.tgz#faeed93cf9203f102e2158c29edcddda320abe82" integrity sha512-RIyUu9lnJIyIaMe63O7/aFv/T2v3KsMFuXMBbUQCHX+cgtGro86ETDj5ed0a8gQL2+DFjzYYsgVG4I36/cUwgw== +lint-staged@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" + integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + dedent "^0.7.0" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -3363,6 +3473,24 @@ lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + loglevel@^1.6.8: version "1.7.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" @@ -3477,7 +3605,7 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0, micromatch@^4.0.2: +micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -3842,7 +3970,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -3918,6 +4046,13 @@ p-map@^2.0.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-retry@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" @@ -4171,6 +4306,13 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + portfinder@^1.0.26: version "1.0.28" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" @@ -4240,6 +4382,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + pretty-error@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" @@ -4569,6 +4716,14 @@ resolve@^1.10.0, resolve@^1.9.0: is-core-module "^2.2.0" path-parse "^1.0.6" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -4613,7 +4768,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^6.6.3: +rxjs@^6.6.3, rxjs@^6.6.7: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -4703,6 +4858,11 @@ selfsigned@^1.10.8: dependencies: node-forge "^0.10.0" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + "semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -4843,7 +5003,7 @@ shell-quote@^1.6.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -signal-exit@^3.0.0, signal-exit@^3.0.3: +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -4866,6 +5026,15 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -5097,6 +5266,11 @@ stream-http@^3.2.0: readable-stream "^3.6.0" xtend "^4.0.2" +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -5106,7 +5280,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== @@ -5154,6 +5328,15 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -5329,6 +5512,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -5449,6 +5637,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -5836,6 +6029,24 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -5873,7 +6084,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: +yaml@^1.10.0, yaml@^1.7.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== diff --git a/package.json b/package.json new file mode 100644 index 00000000..ea1d9854 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "husky": "^6.0.0" + }, + "scripts": { + "prepare": "husky install" + } +} diff --git a/pusher/.prettierignore b/pusher/.prettierignore new file mode 100644 index 00000000..1f453464 --- /dev/null +++ b/pusher/.prettierignore @@ -0,0 +1 @@ +src/Messages/generated diff --git a/pusher/.prettierrc.json b/pusher/.prettierrc.json new file mode 100644 index 00000000..e8980d15 --- /dev/null +++ b/pusher/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "tabWidth": 4 +} diff --git a/pusher/package.json b/pusher/package.json index 63188032..8ad227ae 100644 --- a/pusher/package.json +++ b/pusher/package.json @@ -11,7 +11,10 @@ "profile": "tsc && node --prof ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", - "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts" + "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", + "precommit": "lint-staged", + "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", + "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" }, "repository": { "type": "git", @@ -65,7 +68,14 @@ "@typescript-eslint/parser": "^2.26.0", "eslint": "^6.8.0", "jasmine": "^3.5.0", + "lint-staged": "^11.0.0", + "prettier": "^2.3.1", "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3" + }, + "lint-staged": { + "*.ts": [ + "prettier --write" + ] } } diff --git a/pusher/src/App.ts b/pusher/src/App.ts index 7a272404..81aed045 100644 --- a/pusher/src/App.ts +++ b/pusher/src/App.ts @@ -1,11 +1,11 @@ // lib/app.ts -import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..." -import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..." -import {MapController} from "./Controller/MapController"; -import {PrometheusController} from "./Controller/PrometheusController"; -import {DebugController} from "./Controller/DebugController"; -import {App as uwsApp} from "./Server/sifrr.server"; -import {AdminController} from "./Controller/AdminController"; +import { IoSocketController } from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..." +import { AuthenticateController } from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..." +import { MapController } from "./Controller/MapController"; +import { PrometheusController } from "./Controller/PrometheusController"; +import { DebugController } from "./Controller/DebugController"; +import { App as uwsApp } from "./Server/sifrr.server"; +import { AdminController } from "./Controller/AdminController"; class App { public app: uwsApp; diff --git a/pusher/src/Controller/AdminController.ts b/pusher/src/Controller/AdminController.ts index 74d4e792..ec1bd067 100644 --- a/pusher/src/Controller/AdminController.ts +++ b/pusher/src/Controller/AdminController.ts @@ -1,19 +1,21 @@ -import {BaseController} from "./BaseController"; -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {apiClientRepository} from "../Services/ApiClientRepository"; -import {AdminRoomMessage, WorldFullWarningToRoomMessage, RefreshRoomPromptMessage} from "../Messages/generated/messages_pb"; +import { BaseController } from "./BaseController"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { apiClientRepository } from "../Services/ApiClientRepository"; +import { + AdminRoomMessage, + WorldFullWarningToRoomMessage, + RefreshRoomPromptMessage, +} from "../Messages/generated/messages_pb"; - -export class AdminController extends BaseController{ - - constructor(private App : TemplatedApp) { +export class AdminController extends BaseController { + constructor(private App: TemplatedApp) { super(); this.App = App; this.receiveGlobalMessagePrompt(); this.receiveRoomEditionPrompt(); } - + receiveRoomEditionPrompt() { this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -23,25 +25,25 @@ export class AdminController extends BaseController{ // eslint-disable-next-line @typescript-eslint/no-misused-promises this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => { 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(); if (token !== ADMIN_API_TOKEN) { - console.error('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.error("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } try { - if (typeof body.roomId !== 'string') { - throw 'Incorrect roomId parameter' + if (typeof body.roomId !== "string") { + throw "Incorrect roomId parameter"; } const roomId: string = body.roomId; - await apiClientRepository.getClient(roomId).then((roomClient) =>{ + await apiClientRepository.getClient(roomId).then((roomClient) => { return new Promise((res, rej) => { const roomMessage = new RefreshRoomPromptMessage(); roomMessage.setRoomid(roomId); @@ -57,12 +59,10 @@ export class AdminController extends BaseController{ } res.writeStatus("200"); - res.end('ok'); - - + res.end("ok"); }); } - + receiveGlobalMessagePrompt() { this.App.options("/message", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -71,59 +71,57 @@ export class AdminController extends BaseController{ // eslint-disable-next-line @typescript-eslint/no-misused-promises this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => { - 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(); - + if (token !== ADMIN_API_TOKEN) { - console.error('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.error("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } try { - if (typeof body.text !== 'string') { - throw 'Incorrect text parameter' + if (typeof body.text !== "string") { + throw "Incorrect text parameter"; } - if (body.type !== 'capacity' && body.type !== 'message') { - throw 'Incorrect type parameter' + if (body.type !== "capacity" && body.type !== "message") { + throw "Incorrect type parameter"; } - if (!body.targets || typeof body.targets !== 'object') { - throw 'Incorrect targets parameter' + if (!body.targets || typeof body.targets !== "object") { + throw "Incorrect targets parameter"; } const text: string = body.text; const type: string = body.type; const targets: string[] = body.targets; - await Promise.all(targets.map((roomId) => { - return apiClientRepository.getClient(roomId).then((roomClient) =>{ - return new Promise((res, rej) => { - if (type === 'message') { - const roomMessage = new AdminRoomMessage(); - roomMessage.setMessage(text); - roomMessage.setRoomid(roomId); + await Promise.all( + targets.map((roomId) => { + return apiClientRepository.getClient(roomId).then((roomClient) => { + return new Promise((res, rej) => { + if (type === "message") { + const roomMessage = new AdminRoomMessage(); + roomMessage.setMessage(text); + roomMessage.setRoomid(roomId); - roomClient.sendAdminMessageToRoom(roomMessage, (err) => { - err ? rej(err) : res(); - }); - } else if (type === 'capacity') { - const roomMessage = new WorldFullWarningToRoomMessage(); - roomMessage.setRoomid(roomId); - - roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => { - err ? rej(err) : res(); - }); - } + roomClient.sendAdminMessageToRoom(roomMessage, (err) => { + err ? rej(err) : res(); + }); + } else if (type === "capacity") { + const roomMessage = new WorldFullWarningToRoomMessage(); + roomMessage.setRoomid(roomId); + roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => { + err ? rej(err) : res(); + }); + } + }); }); - }); - })); - + }) + ); } catch (err) { this.errorToResponse(err, res); return; @@ -131,7 +129,7 @@ export class AdminController extends BaseController{ res.writeStatus("200"); this.addCorsHeaders(res); - res.end('ok'); + res.end("ok"); }); } } diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 317848c0..3012e275 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -1,17 +1,16 @@ -import { v4 } from 'uuid'; -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {BaseController} from "./BaseController"; -import {adminApi} from "../Services/AdminApi"; -import {jwtTokenManager} from "../Services/JWTTokenManager"; -import {parse} from "query-string"; +import { v4 } from "uuid"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { BaseController } from "./BaseController"; +import { adminApi } from "../Services/AdminApi"; +import { jwtTokenManager } from "../Services/JWTTokenManager"; +import { parse } from "query-string"; export interface TokenInterface { - userUuid: string + userUuid: string; } export class AuthenticateController extends BaseController { - - constructor(private App : TemplatedApp) { + constructor(private App: TemplatedApp) { super(); this.register(); this.verify(); @@ -19,7 +18,7 @@ export class AuthenticateController extends BaseController { } //Try to login with an admin token - private register(){ + private register() { this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -29,15 +28,15 @@ export class AuthenticateController extends BaseController { this.App.post("/register", (res: HttpResponse, req: HttpRequest) => { (async () => { res.onAborted(() => { - console.warn('Login request was aborted'); - }) + console.warn("Login request was aborted"); + }); const param = await res.json(); //todo: what to do if the organizationMemberToken is already used? - const organizationMemberToken:string|null = param.organizationMemberToken; + const organizationMemberToken: string | null = param.organizationMemberToken; 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 userUuid = data.userUuid; const organizationSlug = data.organizationSlug; @@ -49,28 +48,26 @@ export class AuthenticateController extends BaseController { const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - authToken, - userUuid, - organizationSlug, - worldSlug, - roomSlug, - mapUrlStart, - organizationMemberToken, - textures - })); - + res.end( + JSON.stringify({ + authToken, + userUuid, + organizationSlug, + worldSlug, + roomSlug, + mapUrlStart, + organizationMemberToken, + textures, + }) + ); } catch (e) { this.errorToResponse(e, res); } - - })(); }); - } - private verify(){ + private verify() { this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -82,50 +79,55 @@ export class AuthenticateController extends BaseController { const query = parse(req.getQuery()); res.onAborted(() => { - console.warn('verify request was aborted'); - }) + console.warn("verify request was aborted"); + }); try { await jwtTokenManager.getUserUuidFromToken(query.token as string); } catch (e) { res.writeStatus("400 Bad Request"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - "success": false, - "message": "Invalid JWT token" - })); + res.end( + JSON.stringify({ + success: false, + message: "Invalid JWT token", + }) + ); return; } res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - "success": true - })); + res.end( + JSON.stringify({ + success: true, + }) + ); })(); }); - } //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.addCorsHeaders(res); res.end(); }); - this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { res.onAborted(() => { - console.warn('Login request was aborted'); - }) + console.warn("Login request was aborted"); + }); const userUuid = v4(); const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - authToken, - userUuid, - })); + res.end( + JSON.stringify({ + authToken, + userUuid, + }) + ); }); } } diff --git a/pusher/src/Controller/BaseController.ts b/pusher/src/Controller/BaseController.ts index 91882138..ce378a55 100644 --- a/pusher/src/Controller/BaseController.ts +++ b/pusher/src/Controller/BaseController.ts @@ -1,11 +1,10 @@ -import {HttpResponse} from "uWebSockets.js"; - +import { HttpResponse } from "uWebSockets.js"; export class BaseController { protected addCorsHeaders(res: HttpResponse): void { - 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-origin', '*'); + 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-origin", "*"); } /** @@ -16,23 +15,23 @@ export class BaseController { if (e && e.message) { let url = e?.config?.url; if (url !== undefined) { - url = ' for URL: '+url; + url = " for URL: " + url; } else { - url = ''; + url = ""; } - console.error('ERROR: '+e.message+url); - } else if (typeof(e) === 'string') { + console.error("ERROR: " + e.message + url); + } else if (typeof e === "string") { console.error(e); } if (e.stack) { console.error(e.stack); } if (e.response) { - res.writeStatus(e.response.status+" "+e.response.statusText); + res.writeStatus(e.response.status + " " + e.response.statusText); 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 { - res.writeStatus("500 Internal Server Error") + res.writeStatus("500 Internal Server Error"); this.addCorsHeaders(res); res.end("An error occurred"); } diff --git a/pusher/src/Controller/DebugController.ts b/pusher/src/Controller/DebugController.ts index af2db139..0b0d188b 100644 --- a/pusher/src/Controller/DebugController.ts +++ b/pusher/src/Controller/DebugController.ts @@ -1,45 +1,46 @@ -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {IoSocketController} from "_Controller/IoSocketController"; -import {stringify} from "circular-json"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -import { parse } from 'query-string'; -import {App} from "../Server/sifrr.server"; -import {socketManager} from "../Services/SocketManager"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { IoSocketController } from "_Controller/IoSocketController"; +import { stringify } from "circular-json"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +import { parse } from "query-string"; +import { App } from "../Server/sifrr.server"; +import { socketManager } from "../Services/SocketManager"; export class DebugController { - constructor(private App : App) { + constructor(private App: App) { this.getDump(); } - - getDump(){ + getDump() { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { const query = parse(req.getQuery()); 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( - socketManager.getWorlds(), - (key: unknown, value: unknown) => { - 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) { + return res + .writeStatus("200 OK") + .writeHeader("Content-Type", "application/json") + .end( + stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { + 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 = []; for (const [setKey, setValue] of value.entries()) { obj.push(setValue); } return obj; - } else { - return value; - } - } - )); + } else { + return value; + } + }) + ); }); } } diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index b2079953..1af9d917 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -1,6 +1,6 @@ -import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." -import {GameRoomPolicyTypes} from "../Model/PusherRoom"; -import {PointInterface} from "../Model/Websocket/PointInterface"; +import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." +import { GameRoomPolicyTypes } from "../Model/PusherRoom"; +import { PointInterface } from "../Model/Websocket/PointInterface"; import { SetPlayerDetailsMessage, SubMessage, @@ -18,17 +18,17 @@ import { CompanionMessage, EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {UserMovesMessage} from "../Messages/generated/messages_pb"; -import {TemplatedApp} from "uWebSockets.js" -import {parse} from "query-string"; -import {jwtTokenManager} from "../Services/JWTTokenManager"; -import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; -import {SocketManager, socketManager} from "../Services/SocketManager"; -import {emitInBatch} from "../Services/IoSocketHelpers"; -import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable"; -import {Zone} from "_Model/Zone"; -import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; -import {v4} from "uuid"; +import { UserMovesMessage } from "../Messages/generated/messages_pb"; +import { TemplatedApp } from "uWebSockets.js"; +import { parse } from "query-string"; +import { jwtTokenManager } from "../Services/JWTTokenManager"; +import { adminApi, CharacterTexture, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; +import { SocketManager, socketManager } from "../Services/SocketManager"; +import { emitInBatch } from "../Services/IoSocketHelpers"; +import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable"; +import { Zone } from "_Model/Zone"; +import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; +import { v4 } from "uuid"; export class IoSocketController { private nextUserId: number = 1; @@ -39,32 +39,29 @@ export class IoSocketController { } adminRoomSocket() { - this.app.ws('/admin/rooms', { + this.app.ws("/admin/rooms", { upgrade: (res, req, context) => { const query = parse(req.getQuery()); - const websocketKey = req.getHeader('sec-websocket-key'); - const websocketProtocol = req.getHeader('sec-websocket-protocol'); - const websocketExtensions = req.getHeader('sec-websocket-extensions'); + const websocketKey = req.getHeader("sec-websocket-key"); + const websocketProtocol = req.getHeader("sec-websocket-protocol"); + const websocketExtensions = req.getHeader("sec-websocket-extensions"); const token = query.token; if (token !== ADMIN_API_TOKEN) { - console.log('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.log("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } const roomId = query.roomId; - if (typeof roomId !== 'string') { - console.error('Received') - res.writeStatus("400 Bad Request").end('Missing room id'); + if (typeof roomId !== "string") { + console.error("Received"); + res.writeStatus("400 Bad Request").end("Missing room id"); return; } - res.upgrade( - {roomId}, - websocketKey, websocketProtocol, websocketExtensions, context, - ); + res.upgrade({ roomId }, websocketKey, websocketProtocol, websocketExtensions, context); }, open: (ws) => { - console.log('Admin socket connect for room: '+ws.roomId); + console.log("Admin socket connect for room: " + ws.roomId); ws.disconnecting = false; socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string); @@ -74,24 +71,34 @@ export class IoSocketController { const roomId = ws.roomId as string; //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))); - if(message.event === 'user-message') { - const messageToEmit = (message.message as { message: string, type: string, userUuid: string }); - if(messageToEmit.type === 'banned'){ - socketManager.emitBan(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string); + if (message.event === "user-message") { + const messageToEmit = message.message as { message: string; type: string; userUuid: string }; + if (messageToEmit.type === "banned") { + socketManager.emitBan( + messageToEmit.userUuid, + messageToEmit.message, + messageToEmit.type, + ws.roomId as string + ); } - if(messageToEmit.type === 'ban') { - socketManager.emitSendUserMessage(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string); + if (messageToEmit.type === "ban") { + socketManager.emitSendUserMessage( + messageToEmit.userUuid, + messageToEmit.message, + messageToEmit.type, + ws.roomId as string + ); } } - }catch (err) { + } catch (err) { console.error(err); } }, close: (ws, code, message) => { - const Client = (ws as ExAdminSocketInterface); + const Client = ws as ExAdminSocketInterface; try { Client.disconnecting = true; socketManager.leaveAdminRoom(Client); @@ -99,12 +106,12 @@ export class IoSocketController { console.error('An error occurred on admin "disconnect"'); console.error(e); } - } - }) + }, + }); } ioConnection() { - this.app.ws('/room', { + this.app.ws("/room", { /* Options */ //compression: uWS.SHARED_COMPRESSOR, idleTimeout: SOCKET_IDLE_TIMER, @@ -114,7 +121,7 @@ export class IoSocketController { upgrade: (res, req, context) => { (async () => { /* Keep track of abortions */ - const upgradeAborted = {aborted: false}; + const upgradeAborted = { aborted: false }; res.onAborted(() => { /* We can simply signal that we were aborted */ @@ -123,15 +130,15 @@ export class IoSocketController { const url = req.getUrl(); const query = parse(req.getQuery()); - const websocketKey = req.getHeader('sec-websocket-key'); - const websocketProtocol = req.getHeader('sec-websocket-protocol'); - const websocketExtensions = req.getHeader('sec-websocket-extensions'); - const IPAddress = req.getHeader('x-forwarded-for'); + const websocketKey = req.getHeader("sec-websocket-key"); + const websocketProtocol = req.getHeader("sec-websocket-protocol"); + const websocketExtensions = req.getHeader("sec-websocket-extensions"); + const IPAddress = req.getHeader("x-forwarded-for"); const roomId = query.roomId; try { - if (typeof roomId !== 'string') { - throw new Error('Undefined room ID: '); + if (typeof roomId !== "string") { + throw new Error("Undefined room ID: "); } const token = query.token; @@ -143,62 +150,69 @@ export class IoSocketController { const right = Number(query.right); 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.setName(query.companion); } - if (typeof name !== 'string') { - throw new Error('Expecting name'); + if (typeof name !== "string") { + throw new Error("Expecting name"); } - if (name === '') { - throw new Error('No empty name'); + if (name === "") { + throw new Error("No empty name"); } let characterLayers = query.characterLayers; if (characterLayers === null) { - throw new Error('Expecting skin'); + throw new Error("Expecting skin"); } - if (typeof characterLayers === 'string') { - characterLayers = [ characterLayers ]; + if (typeof characterLayers === "string") { + characterLayers = [characterLayers]; } const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId); let memberTags: string[] = []; - let memberVisitCardUrl: string|null = null; + let memberVisitCardUrl: string | null = null; let memberMessages: unknown; let memberTextures: CharacterTexture[] = []; const room = await socketManager.getOrCreateRoom(roomId); if (ADMIN_API_URL) { try { - let userData : FetchMemberDataByUuidResponse = { + let userData: FetchMemberDataByUuidResponse = { uuid: v4(), tags: [], visitCardUrl: null, textures: [], messages: [], - anonymous: true + anonymous: true, }; try { userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId); - }catch (err){ + } catch (err) { if (err?.response?.status == 404) { // 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.'); - } else if(err?.response?.status == 403) { + console.warn( + '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. // we finish immediately the upgrade then we will close the socket as soon as it starts opening. - return res.upgrade({ - rejected: true, - message: err?.response?.data.message, - status: err?.response?.status - }, websocketKey, - websocketProtocol, - websocketExtensions, - context); - }else{ + return res.upgrade( + { + rejected: true, + message: err?.response?.data.message, + status: err?.response?.status, + }, + websocketKey, + websocketProtocol, + websocketExtensions, + context + ); + } else { throw err; } } @@ -206,21 +220,30 @@ export class IoSocketController { memberTags = userData.tags; memberVisitCardUrl = userData.visitCardUrl; memberTextures = userData.textures; - if (!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.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) { - throw new Error('Use the login URL to connect') + if ( + !room.public && + room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && + userData.anonymous === true + ) { + throw new Error("Use the login URL to connect"); } } 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); - throw new Error('User cannot access this world') + throw new Error("User cannot access this world"); } } // Generate characterLayers objects from characterLayers string[] - const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures); + const characterLayerObjs: CharacterLayer[] = + SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures); if (upgradeAborted.aborted) { 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 */ - res.upgrade({ + res.upgrade( + { // Data passed here is accessible on the "websocket" socket object. url, token, @@ -246,22 +270,22 @@ export class IoSocketController { position: { x: x, y: y, - direction: 'down', - moving: false + direction: "down", + moving: false, } as PointInterface, viewport: { top, right, bottom, - left - } + left, + }, }, /* Spell these correctly */ websocketKey, websocketProtocol, websocketExtensions, - context); - + context + ); } catch (e) { /*if (e instanceof Error) { console.log(e.message); @@ -269,23 +293,26 @@ export class IoSocketController { } else { res.writeStatus("500 Internal Server Error").end('An error occurred'); }*/ - return res.upgrade({ - rejected: true, - message: e.message ? e.message : '500 Internal Server Error' - }, websocketKey, - websocketProtocol, - websocketExtensions, - context); + return res.upgrade( + { + rejected: true, + message: e.message ? e.message : "500 Internal Server Error", + }, + websocketKey, + websocketProtocol, + websocketExtensions, + context + ); } })(); }, /* Handlers */ open: (ws) => { - if(ws.rejected === true) { + if (ws.rejected === true) { //FIX ME to use status code - if(ws.message === 'World is full'){ + if (ws.message === "World is full") { socketManager.emitWorldFullMessage(ws); - }else{ + } else { socketManager.emitConnexionErrorMessage(ws, ws.message as string); } ws.close(); @@ -299,7 +326,7 @@ export class IoSocketController { //get data information and show messages if (client.messages && Array.isArray(client.messages)) { 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(); sendUserMessage.setType(messageToSend.type); @@ -323,33 +350,48 @@ export class IoSocketController { } else if (message.hasUsermovesmessage()) { socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); } else if (message.hasSetplayerdetailsmessage()) { - socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage); + socketManager.handleSetPlayerDetails( + client, + message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage + ); } else if (message.hasSilentmessage()) { socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitVideo( + client, + message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitScreenSharing( + client, + message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasPlayglobalmessage()) { socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage); - } else if (message.hasReportplayermessage()){ + } else if (message.hasReportplayermessage()) { socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); - } else if (message.hasQueryjitsijwtmessage()){ - socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); - } else if (message.hasEmotepromptmessage()){ - socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage); + } else if (message.hasQueryjitsijwtmessage()) { + socketManager.handleQueryJitsiJwtMessage( + client, + 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); }, drain: (ws) => { - console.log('WebSocket backpressure: ' + ws.getBufferedAmount()); + console.log("WebSocket backpressure: " + ws.getBufferedAmount()); }, close: (ws, code, message) => { - const Client = (ws as ExSocketInterface); + const Client = ws as ExSocketInterface; try { Client.disconnecting = true; //leave room @@ -358,13 +400,13 @@ export class IoSocketController { console.error('An error occurred on "disconnect"'); console.error(e); } - } - }) + }, + }); } //eslint-disable-next-line @typescript-eslint/no-explicit-any private initClient(ws: any): ExSocketInterface { - const client : ExSocketInterface = ws; + const client: ExSocketInterface = ws; client.userId = this.nextUserId; this.nextUserId++; client.userUuid = ws.userUuid; @@ -374,7 +416,7 @@ export class IoSocketController { client.batchTimeout = null; client.emitInBatch = (payload: SubMessage): void => { emitInBatch(client, payload); - } + }; client.disconnecting = false; client.messages = ws.messages; diff --git a/pusher/src/Controller/MapController.ts b/pusher/src/Controller/MapController.ts index 1ce04265..1df828d4 100644 --- a/pusher/src/Controller/MapController.ts +++ b/pusher/src/Controller/MapController.ts @@ -1,18 +1,15 @@ -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {BaseController} from "./BaseController"; -import {parse} from "query-string"; -import {adminApi} from "../Services/AdminApi"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { BaseController } from "./BaseController"; +import { parse } from "query-string"; +import { adminApi } from "../Services/AdminApi"; - -export class MapController extends BaseController{ - - constructor(private App : TemplatedApp) { +export class MapController extends BaseController { + constructor(private App: TemplatedApp) { super(); this.App = App; this.getMapUrl(); } - // Returns a map mapping map name to file name of the map getMapUrl() { 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) => { - res.onAborted(() => { - console.warn('/map request was aborted'); - }) + console.warn("/map request was aborted"); + }); const query = parse(req.getQuery()); - if (typeof query.organizationSlug !== 'string') { - console.error('Expected organizationSlug parameter'); + if (typeof query.organizationSlug !== "string") { + console.error("Expected organizationSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected organizationSlug parameter"); return; } - if (typeof query.worldSlug !== 'string') { - console.error('Expected worldSlug parameter'); + if (typeof query.worldSlug !== "string") { + console.error("Expected worldSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected worldSlug parameter"); return; } - if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { - console.error('Expected only one roomSlug parameter'); + if (typeof query.roomSlug !== "string" && query.roomSlug !== undefined) { + console.error("Expected only one roomSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected only one roomSlug parameter"); @@ -53,7 +49,11 @@ export class MapController extends BaseController{ (async () => { 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"); this.addCorsHeaders(res); @@ -62,7 +62,6 @@ export class MapController extends BaseController{ this.errorToResponse(e, res); } })(); - }); } } diff --git a/pusher/src/Controller/PrometheusController.ts b/pusher/src/Controller/PrometheusController.ts index e854cf43..3ab3d33f 100644 --- a/pusher/src/Controller/PrometheusController.ts +++ b/pusher/src/Controller/PrometheusController.ts @@ -1,7 +1,7 @@ -import {App} from "../Server/sifrr.server"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -const register = require('prom-client').register; -const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; +import { App } from "../Server/sifrr.server"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +const register = require("prom-client").register; +const collectDefaultMetrics = require("prom-client").collectDefaultMetrics; export class PrometheusController { constructor(private App: App) { @@ -14,7 +14,7 @@ export class PrometheusController { } private metrics(res: HttpResponse, req: HttpRequest): void { - res.writeHeader('Content-Type', register.contentType); + res.writeHeader("Content-Type", register.contentType); res.end(register.metrics()); } } diff --git a/pusher/src/Enum/EnvironmentVariable.ts b/pusher/src/Enum/EnvironmentVariable.ts index 5b3ec9c4..be974697 100644 --- a/pusher/src/Enum/EnvironmentVariable.ts +++ b/pusher/src/Enum/EnvironmentVariable.ts @@ -1,16 +1,16 @@ const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY"; const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; -const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; -const API_URL = process.env.API_URL || ''; -const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; -const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600; +const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; +const API_URL = process.env.API_URL || ""; +const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; +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 JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; -const JITSI_ISS = process.env.JITSI_ISS || ''; -const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; -const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || '8080') || 8080 +const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; +const JITSI_ISS = process.env.JITSI_ISS || ""; +const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; +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 { @@ -26,5 +26,5 @@ export { JITSI_URL, JITSI_ISS, SECRET_JITSI_KEY, - PUSHER_HTTP_PORT -} + PUSHER_HTTP_PORT, +}; diff --git a/pusher/src/Model/Movable.ts b/pusher/src/Model/Movable.ts index 173db0ae..ca586b7c 100644 --- a/pusher/src/Model/Movable.ts +++ b/pusher/src/Model/Movable.ts @@ -1,8 +1,8 @@ -import {PositionInterface} from "_Model/PositionInterface"; +import { PositionInterface } from "_Model/PositionInterface"; /** * A physical object that can be placed into a Zone */ export interface Movable { - getPosition(): PositionInterface + getPosition(): PositionInterface; } diff --git a/pusher/src/Model/PositionDispatcher.ts b/pusher/src/Model/PositionDispatcher.ts index 1150394b..594328e3 100644 --- a/pusher/src/Model/PositionDispatcher.ts +++ b/pusher/src/Model/PositionDispatcher.ts @@ -8,9 +8,9 @@ * 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. */ -import {Zone, ZoneEventListener} from "./Zone"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import { Zone, ZoneEventListener } from "./Zone"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; //import Debug from "debug"; //const debug = Debug('positiondispatcher'); @@ -21,19 +21,22 @@ interface ZoneDescriptor { } export class PositionDispatcher { - // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) 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 { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), - } + }; } /** @@ -41,7 +44,7 @@ export class PositionDispatcher { */ public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void { if (viewport.left > viewport.right || viewport.top > viewport.bottom) { - console.warn('Invalid viewport received: ', viewport); + console.warn("Invalid viewport received: ", viewport); return; } @@ -57,8 +60,8 @@ export class PositionDispatcher { } } - const addedZones = [...newZones].filter(x => !oldZones.has(x)); - const removedZones = [...oldZones].filter(x => !newZones.has(x)); + const addedZones = [...newZones].filter((x) => !oldZones.has(x)); + const removedZones = [...oldZones].filter((x) => !newZones.has(x)); for (const zone of addedZones) { zone.startListening(socket); diff --git a/pusher/src/Model/PositionInterface.ts b/pusher/src/Model/PositionInterface.ts index d3b0dd47..65636759 100644 --- a/pusher/src/Model/PositionInterface.ts +++ b/pusher/src/Model/PositionInterface.ts @@ -1,4 +1,4 @@ export interface PositionInterface { - x: number, - y: number + x: number; + y: number; } diff --git a/pusher/src/Model/PusherRoom.ts b/pusher/src/Model/PusherRoom.ts index dcea5859..a49fce3e 100644 --- a/pusher/src/Model/PusherRoom.ts +++ b/pusher/src/Model/PusherRoom.ts @@ -1,9 +1,9 @@ -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; -import {PositionDispatcher} from "./PositionDispatcher"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; -import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; -import {arrayIntersect} from "../Services/ArrayHelper"; -import {ZoneEventListener} from "_Model/Zone"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; +import { PositionDispatcher } from "./PositionDispatcher"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; +import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; +import { arrayIntersect } from "../Services/ArrayHelper"; +import { ZoneEventListener } from "_Model/Zone"; export enum GameRoomPolicyTypes { ANONYMUS_POLICY = 1, @@ -17,13 +17,11 @@ export class PusherRoom { public tags: string[]; public policyType: GameRoomPolicyTypes; public readonly roomSlug: string; - public readonly worldSlug: string = ''; - public readonly organizationSlug: string = ''; + public readonly worldSlug: string = ""; + public readonly organizationSlug: string = ""; private versionNumber: number = 1; - constructor(public readonly roomId: string, - private socketListener: ZoneEventListener) - { + constructor(public readonly roomId: string, private socketListener: ZoneEventListener) { this.public = isRoomAnonymous(roomId); this.tags = []; this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; @@ -31,7 +29,7 @@ export class PusherRoom { if (this.public) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { - const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId); this.roomSlug = roomSlug; this.organizationSlug = organizationSlug; this.worldSlug = worldSlug; @@ -41,11 +39,11 @@ export class PusherRoom { 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); } - public leave(socket : ExSocketInterface){ + public leave(socket: ExSocketInterface) { this.positionNotifier.removeViewport(socket); } diff --git a/pusher/src/Model/RoomIdentifier.ts b/pusher/src/Model/RoomIdentifier.ts index 3ac62bca..d1de8800 100644 --- a/pusher/src/Model/RoomIdentifier.ts +++ b/pusher/src/Model/RoomIdentifier.ts @@ -1,30 +1,30 @@ //helper functions to parse room IDs export const isRoomAnonymous = (roomID: string): boolean => { - if (roomID.startsWith('_/')) { + if (roomID.startsWith("_/")) { return true; - } else if(roomID.startsWith('@/')) { + } else if (roomID.startsWith("@/")) { return false; } else { - throw new Error('Incorrect room ID: '+roomID); + throw new Error("Incorrect room ID: " + roomID); } -} +}; export const extractRoomSlugPublicRoomId = (roomId: string): string => { - const idParts = roomId.split('/'); - if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); - return idParts.slice(2).join('/'); -} + const idParts = roomId.split("/"); + if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId); + return idParts.slice(2).join("/"); +}; export interface extractDataFromPrivateRoomIdResponse { organizationSlug: string; worldSlug: string; roomSlug: string; } export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { - const idParts = roomId.split('/'); - if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const idParts = roomId.split("/"); + if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId); const organizationSlug = idParts[1]; const worldSlug = idParts[2]; const roomSlug = idParts[3]; - return {organizationSlug, worldSlug, roomSlug} -} \ No newline at end of file + return { organizationSlug, worldSlug, roomSlug }; +}; diff --git a/pusher/src/Model/Websocket/ExAdminSocketInterface.ts b/pusher/src/Model/Websocket/ExAdminSocketInterface.ts index 1e03db6c..7599c82c 100644 --- a/pusher/src/Model/Websocket/ExAdminSocketInterface.ts +++ b/pusher/src/Model/Websocket/ExAdminSocketInterface.ts @@ -1,21 +1,22 @@ -import {PointInterface} from "./PointInterface"; -import {Identificable} from "./Identificable"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import { PointInterface } from "./PointInterface"; +import { Identificable } from "./Identificable"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; import { AdminPusherToBackMessage, BatchMessage, - PusherToBackMessage, ServerToAdminClientMessage, + PusherToBackMessage, + ServerToAdminClientMessage, ServerToClientMessage, - SubMessage + SubMessage, } from "../../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js" -import {CharacterTexture} from "../../Services/AdminApi"; -import {ClientDuplexStream} from "grpc"; -import {Zone} from "_Model/Zone"; +import { WebSocket } from "uWebSockets.js"; +import { CharacterTexture } from "../../Services/AdminApi"; +import { ClientDuplexStream } from "grpc"; +import { Zone } from "_Model/Zone"; export type AdminConnection = ClientDuplexStream; export interface ExAdminSocketInterface extends WebSocket { - adminConnection: AdminConnection, - disconnecting: boolean, + adminConnection: AdminConnection; + disconnecting: boolean; } diff --git a/pusher/src/Model/Websocket/ExSocketInterface.ts b/pusher/src/Model/Websocket/ExSocketInterface.ts index 98759142..9a92a0e7 100644 --- a/pusher/src/Model/Websocket/ExSocketInterface.ts +++ b/pusher/src/Model/Websocket/ExSocketInterface.ts @@ -1,23 +1,23 @@ -import {PointInterface} from "./PointInterface"; -import {Identificable} from "./Identificable"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import { PointInterface } from "./PointInterface"; +import { Identificable } from "./Identificable"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; import { BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, - SubMessage + SubMessage, } from "../../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js" -import {CharacterTexture} from "../../Services/AdminApi"; -import {ClientDuplexStream} from "grpc"; -import {Zone} from "_Model/Zone"; +import { WebSocket } from "uWebSockets.js"; +import { CharacterTexture } from "../../Services/AdminApi"; +import { ClientDuplexStream } from "grpc"; +import { Zone } from "_Model/Zone"; export type BackConnection = ClientDuplexStream; export interface CharacterLayer { - name: string, - url: string|undefined + name: string; + url: string | undefined; } export interface ExSocketInterface extends WebSocket, Identificable { @@ -36,12 +36,12 @@ export interface ExSocketInterface extends WebSocket, Identificable { */ emitInBatch: (payload: SubMessage) => void; batchedMessages: BatchMessage; - batchTimeout: NodeJS.Timeout|null; - disconnecting: boolean, - messages: unknown, - tags: string[], - visitCardUrl: string|null, - textures: CharacterTexture[], - backConnection: BackConnection, + batchTimeout: NodeJS.Timeout | null; + disconnecting: boolean; + messages: unknown; + tags: string[]; + visitCardUrl: string | null; + textures: CharacterTexture[]; + backConnection: BackConnection; listenedZones: Set; } diff --git a/pusher/src/Model/Websocket/ItemEventMessage.ts b/pusher/src/Model/Websocket/ItemEventMessage.ts index b1f9203e..1bb7f615 100644 --- a/pusher/src/Model/Websocket/ItemEventMessage.ts +++ b/pusher/src/Model/Websocket/ItemEventMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isItemEventMessageInterface = - new tg.IsInterface().withProperties({ +export const isItemEventMessageInterface = new tg.IsInterface() + .withProperties({ itemId: tg.isNumber, event: tg.isString, state: tg.isUnknown, parameters: tg.isUnknown, - }).get(); + }) + .get(); export type ItemEventMessageInterface = tg.GuardedType; diff --git a/pusher/src/Model/Websocket/Point.ts b/pusher/src/Model/Websocket/Point.ts index c66720ba..19b57d2e 100644 --- a/pusher/src/Model/Websocket/Point.ts +++ b/pusher/src/Model/Websocket/Point.ts @@ -1,6 +1,10 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; -export class Point implements PointInterface{ - constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { - } +export class Point implements PointInterface { + constructor( + public x: number, + public y: number, + public direction: string = "none", + public moving: boolean = false + ) {} } diff --git a/pusher/src/Model/Websocket/PointInterface.ts b/pusher/src/Model/Websocket/PointInterface.ts index afb07a23..d7c7826e 100644 --- a/pusher/src/Model/Websocket/PointInterface.ts +++ b/pusher/src/Model/Websocket/PointInterface.ts @@ -7,11 +7,12 @@ import * as tg from "generic-type-guard"; readonly moving: boolean; }*/ -export const isPointInterface = - new tg.IsInterface().withProperties({ +export const isPointInterface = new tg.IsInterface() + .withProperties({ x: tg.isNumber, y: tg.isNumber, direction: tg.isString, - moving: tg.isBoolean - }).get(); + moving: tg.isBoolean, + }) + .get(); export type PointInterface = tg.GuardedType; diff --git a/pusher/src/Model/Websocket/ProtobufUtils.ts b/pusher/src/Model/Websocket/ProtobufUtils.ts index 89a90acc..bd9cb9c2 100644 --- a/pusher/src/Model/Websocket/ProtobufUtils.ts +++ b/pusher/src/Model/Websocket/ProtobufUtils.ts @@ -1,34 +1,33 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; import { CharacterLayerMessage, ItemEventMessage, PointMessage, - PositionMessage + PositionMessage, } from "../../Messages/generated/messages_pb"; -import {CharacterLayer, ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import { CharacterLayer, ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; import Direction = PositionMessage.Direction; -import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; -import {PositionInterface} from "_Model/PositionInterface"; +import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; +import { PositionInterface } from "_Model/PositionInterface"; export class ProtobufUtils { - public static toPositionMessage(point: PointInterface): PositionMessage { let direction: Direction; switch (point.direction) { - case 'up': + case "up": direction = Direction.UP; break; - case 'down': + case "down": direction = Direction.DOWN; break; - case 'left': + case "left": direction = Direction.LEFT; break; - case 'right': + case "right": direction = Direction.RIGHT; break; default: - throw new Error('unexpected direction'); + throw new Error("unexpected direction"); } const position = new PositionMessage(); @@ -44,16 +43,16 @@ export class ProtobufUtils { let direction: string; switch (position.getDirection()) { case Direction.UP: - direction = 'up'; + direction = "up"; break; case Direction.DOWN: - direction = 'down'; + direction = "down"; break; case Direction.LEFT: - direction = 'left'; + direction = "left"; break; case Direction.RIGHT: - direction = 'right'; + direction = "right"; break; default: throw new Error("Unexpected direction"); @@ -82,7 +81,7 @@ export class ProtobufUtils { event: itemEventMessage.getEvent(), parameters: JSON.parse(itemEventMessage.getParametersjson()), state: JSON.parse(itemEventMessage.getStatejson()), - } + }; } public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { @@ -96,7 +95,7 @@ export class ProtobufUtils { } public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { - return characterLayers.map(function(characterLayer): CharacterLayerMessage { + return characterLayers.map(function (characterLayer): CharacterLayerMessage { const message = new CharacterLayerMessage(); message.setName(characterLayer.name); if (characterLayer.url) { diff --git a/pusher/src/Model/Websocket/ViewportMessage.ts b/pusher/src/Model/Websocket/ViewportMessage.ts index 62e2fc81..ea71ad68 100644 --- a/pusher/src/Model/Websocket/ViewportMessage.ts +++ b/pusher/src/Model/Websocket/ViewportMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isViewport = - new tg.IsInterface().withProperties({ +export const isViewport = new tg.IsInterface() + .withProperties({ left: tg.isNumber, top: tg.isNumber, right: tg.isNumber, bottom: tg.isNumber, - }).get(); + }) + .get(); export type ViewportInterface = tg.GuardedType; diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 318a119b..8eeeb3ef 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -1,16 +1,23 @@ -import {ExSocketInterface} from "./Websocket/ExSocketInterface"; -import {apiClientRepository} from "../Services/ApiClientRepository"; +import { ExSocketInterface } from "./Websocket/ExSocketInterface"; +import { apiClientRepository } from "../Services/ApiClientRepository"; import { BatchToPusherMessage, - CharacterLayerMessage, GroupLeftZoneMessage, GroupUpdateMessage, GroupUpdateZoneMessage, - PointMessage, PositionMessage, UserJoinedMessage, - UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage, + CharacterLayerMessage, + GroupLeftZoneMessage, + GroupUpdateMessage, + GroupUpdateZoneMessage, + PointMessage, + PositionMessage, + UserJoinedMessage, + UserJoinedZoneMessage, + UserLeftZoneMessage, + UserMovedMessage, ZoneMessage, EmoteEventMessage, - CompanionMessage + CompanionMessage, } from "../Messages/generated/messages_pb"; -import {ClientReadableStream} from "grpc"; -import {PositionDispatcher} from "_Model/PositionDispatcher"; +import { ClientReadableStream } from "grpc"; +import { PositionDispatcher } from "_Model/PositionDispatcher"; import Debug from "debug"; 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 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)) { - 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 { const position = message.getPosition(); 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) { const position = userMovedMessage.getPosition(); if (position === undefined) { - throw new Error('Missing position'); + throw new Error("Missing position"); } this.position = position; } @@ -78,13 +99,12 @@ export class UserDescriptor { } 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 { const position = message.getPosition(); if (position === undefined) { - throw new Error('Missing position'); + throw new Error("Missing position"); } return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position); } @@ -97,7 +117,7 @@ export class GroupDescriptor { public toGroupUpdateMessage(): GroupUpdateMessage { const groupUpdateMessage = new GroupUpdateMessage(); 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.setGroupsize(this.groupSize); @@ -108,8 +128,8 @@ export class GroupDescriptor { } interface ZoneDescriptor { - x: number, - y: number + x: number; + y: number; } export class Zone { @@ -120,21 +140,26 @@ export class Zone { private backConnection!: ClientReadableStream; 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. */ public async init(): Promise { - 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 zoneMessage = new ZoneMessage(); zoneMessage.setRoomid(this.positionDispatcher.roomId); zoneMessage.setX(this.x); zoneMessage.setY(this.y); this.backConnection = apiClient.listenZone(zoneMessage); - this.backConnection.on('data', (batch: BatchToPusherMessage) => { + this.backConnection.on("data", (batch: BatchToPusherMessage) => { for (const message of batch.getPayloadList()) { if (message.hasUserjoinedzonemessage()) { const userJoinedZoneMessage = message.getUserjoinedzonemessage() as UserJoinedZoneMessage; @@ -179,33 +204,32 @@ export class Zone { const userDescriptor = this.users.get(userId); if (userDescriptor === undefined) { - console.error('Unexpected move message received for user "'+userId+'"'); + console.error('Unexpected move message received for user "' + userId + '"'); return; } userDescriptor.update(userMovedMessage); this.notifyUserMove(userDescriptor); - } else if(message.hasEmoteeventmessage()) { + } else if (message.hasEmoteeventmessage()) { const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; this.notifyEmote(emoteEventMessage); } else { - throw new Error('Unexpected message'); + throw new Error("Unexpected message"); } - } }); - this.backConnection.on('error', (e) => { + this.backConnection.on("error", (e) => { if (!this.isClosing) { - debug('Error on back connection') + debug("Error on back connection"); this.close(); this.onBackFailure(e, this); } }); - this.backConnection.on('close', () => { + this.backConnection.on("close", () => { if (!this.isClosing) { - debug('Close on back connection') + debug("Close on back connection"); this.close(); this.onBackFailure(null, this); } @@ -213,7 +237,7 @@ export class Zone { } 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.backConnection.cancel(); } @@ -225,7 +249,7 @@ export class Zone { /** * 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) { if (listener.userId === user.userId) { continue; @@ -241,7 +265,7 @@ export class Zone { /** * 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) { if (oldZone === undefined || !this.isListeningZone(listener, oldZone.x, oldZone.y)) { this.socketListener.onGroupEnters(group, listener); @@ -254,7 +278,7 @@ export class Zone { /** * 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) { if (listener.userId === userId) { continue; @@ -279,7 +303,7 @@ export class Zone { /** * 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) { if (listener.groupId === groupId) { continue; diff --git a/pusher/src/Server/server/app.ts b/pusher/src/Server/server/app.ts index 3b98a9b3..4c422d5c 100644 --- a/pusher/src/Server/server/app.ts +++ b/pusher/src/Server/server/app.ts @@ -1,13 +1,13 @@ -import { App as _App, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { App as _App, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class App extends (_App) { - constructor(options: AppOptions = {}) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions = {}) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default App; diff --git a/pusher/src/Server/server/baseapp.ts b/pusher/src/Server/server/baseapp.ts index accd8a99..6d973ac7 100644 --- a/pusher/src/Server/server/baseapp.ts +++ b/pusher/src/Server/server/baseapp.ts @@ -1,116 +1,109 @@ -import { Readable } from 'stream'; -import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { Readable } from "stream"; +import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; -import formData from './formdata'; -import { stob } from './utils'; -import { Handler } from './types'; -import {join} from "path"; +import formData from "./formdata"; +import { stob } from "./utils"; +import { Handler } from "./types"; +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 handleBody = (res: HttpResponse, req: HttpRequest) => { - const contType = req.getHeader('content-type'); + const contType = req.getHeader("content-type"); - res.bodyStream = function() { - const stream = new Readable(); - stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method + res.bodyStream = function () { + const stream = new Readable(); + stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method - this.onData((ab: ArrayBuffer, isLast: boolean) => { - // 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 - if (isLast) { - stream.push(null); - } - }); + this.onData((ab: ArrayBuffer, isLast: boolean) => { + // 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 + if (isLast) { + stream.push(null); + } + }); - return stream; - }; + return stream; + }; - res.body = () => stob(res.bodyStream()); + res.body = () => stob(res.bodyStream()); - if (contType.includes('application/json')) - res.json = async () => JSON.parse(await res.body()); - if (contTypes.map(t => contType.includes(t)).includes(true)) - res.formData = formData.bind(res, contType); + if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body()); + if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType); }; class BaseApp { - _sockets = new Map(); - ws!: TemplatedApp['ws']; - get!: TemplatedApp['get']; - _post!: TemplatedApp['post']; - _put!: TemplatedApp['put']; - _patch!: TemplatedApp['patch']; - _listen!: TemplatedApp['listen']; + _sockets = new Map(); + ws!: TemplatedApp["ws"]; + get!: TemplatedApp["get"]; + _post!: TemplatedApp["post"]; + _put!: TemplatedApp["put"]; + _patch!: TemplatedApp["patch"]; + _listen!: TemplatedApp["listen"]; - post(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._post(pattern, (res, req) => { - handleBody(res, req); - handler(res, req); - }); - return this; - } + post(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._post(pattern, (res, req) => { + handleBody(res, req); + handler(res, req); + }); + return this; + } - put(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._put(pattern, (res, req) => { - handleBody(res, req); + put(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._put(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - patch(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._patch(pattern, (res, req) => { - handleBody(res, req); + patch(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._patch(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - listen(h: string | number, p: Function | number = noOp, cb?: Function) { - if (typeof p === 'number' && typeof h === 'string') { - this._listen(h, p, socket => { - this._sockets.set(p, socket); - if (cb === undefined) { - throw new Error('cb undefined'); + listen(h: string | number, p: Function | number = noOp, cb?: Function) { + if (typeof p === "number" && typeof h === "string") { + this._listen(h, p, (socket) => { + this._sockets.set(p, socket); + if (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); - }); - } 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; } - return this; - } - - close(port: null | number = null) { - if (port) { - this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); - this._sockets.delete(port); - } else { - this._sockets.forEach(app => { - us_listen_socket_close(app); - }); - this._sockets.clear(); + close(port: null | number = null) { + if (port) { + this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); + this._sockets.delete(port); + } else { + this._sockets.forEach((app) => { + us_listen_socket_close(app); + }); + this._sockets.clear(); + } + return this; } - return this; - } } export default BaseApp; diff --git a/pusher/src/Server/server/formdata.ts b/pusher/src/Server/server/formdata.ts index 9dd08440..66e51db4 100644 --- a/pusher/src/Server/server/formdata.ts +++ b/pusher/src/Server/server/formdata.ts @@ -1,100 +1,99 @@ -import { createWriteStream } from 'fs'; -import { join, dirname } from 'path'; -import Busboy from 'busboy'; -import mkdirp from 'mkdirp'; +import { createWriteStream } from "fs"; +import { join, dirname } from "path"; +import Busboy from "busboy"; +import mkdirp from "mkdirp"; function formData( - contType: string, - options: busboy.BusboyConfig & { - abortOnLimit?: boolean; - tmpDir?: string; - onFile?: ( - fieldname: string, - file: NodeJS.ReadableStream, - filename: string, - encoding: string, - mimetype: string - ) => string; - onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - filename?: (oldName: string) => string; - } = {} + contType: string, + options: busboy.BusboyConfig & { + abortOnLimit?: boolean; + tmpDir?: string; + onFile?: ( + fieldname: string, + file: NodeJS.ReadableStream, + filename: string, + encoding: string, + mimetype: string + ) => string; + onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + filename?: (oldName: string) => string; + } = {} ) { - console.log('Enter form data'); - options.headers = { - 'content-type': contType - }; + console.log("Enter form data"); + options.headers = { + "content-type": contType, + }; - return new Promise((resolve, reject) => { - const busb = new Busboy(options); - const ret = {}; + return new Promise((resolve, reject) => { + const busb = new Busboy(options); + const ret = {}; - this.bodyStream().pipe(busb); + this.bodyStream().pipe(busb); - busb.on('limit', () => { - if (options.abortOnLimit) { - reject(Error('limit')); - } + busb.on("limit", () => { + if (options.abortOnLimit) { + 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( - ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any - fieldname: string, - value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any + ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + fieldname: string, + value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any ) { - if (fieldname.endsWith('[]')) { - fieldname = fieldname.slice(0, fieldname.length - 2); - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); + if (fieldname.endsWith("[]")) { + fieldname = fieldname.slice(0, fieldname.length - 2); + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else { + ret[fieldname] = [value]; + } } 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; diff --git a/pusher/src/Server/server/sslapp.ts b/pusher/src/Server/server/sslapp.ts index 46ae89a5..80df0e4a 100644 --- a/pusher/src/Server/server/sslapp.ts +++ b/pusher/src/Server/server/sslapp.ts @@ -1,13 +1,13 @@ -import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class SSLApp extends (_SSLApp) { - constructor(options: AppOptions) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default SSLApp; diff --git a/pusher/src/Server/server/types.ts b/pusher/src/Server/server/types.ts index 3d0f48c7..afc21d17 100644 --- a/pusher/src/Server/server/types.ts +++ b/pusher/src/Server/server/types.ts @@ -1,9 +1,9 @@ -import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; export type UwsApp = { - (options: AppOptions): TemplatedApp; - new (options: AppOptions): TemplatedApp; - prototype: TemplatedApp; + (options: AppOptions): TemplatedApp; + new (options: AppOptions): TemplatedApp; + prototype: TemplatedApp; }; export type Handler = (res: HttpResponse, req: HttpRequest) => void; diff --git a/pusher/src/Server/server/utils.ts b/pusher/src/Server/server/utils.ts index 80ea3938..dc813064 100644 --- a/pusher/src/Server/server/utils.ts +++ b/pusher/src/Server/server/utils.ts @@ -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 - const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( - Object.keys(from) - ); - ownProps.forEach(prop => { - if (prop === 'constructor' || from[prop] === undefined) return; - if (who[prop] && overwrite) { - who[`_${prop}`] = who[prop]; - } - if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); - else who[prop] = from[prop]; - }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extend(who: any, from: any, overwrite = true) { + const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); + ownProps.forEach((prop) => { + if (prop === "constructor" || from[prop] === undefined) return; + if (who[prop] && overwrite) { + who[`_${prop}`] = who[prop]; + } + if (typeof from[prop] === "function") who[prop] = from[prop].bind(who); + else who[prop] = from[prop]; + }); } function stob(stream: ReadStream): Promise { - return new Promise(resolve => { - const buffers: Buffer[] = []; - stream.on('data', buffers.push.bind(buffers)); + return new Promise((resolve) => { + const buffers: Buffer[] = []; + stream.on("data", buffers.push.bind(buffers)); - stream.on('end', () => { - switch (buffers.length) { - case 0: - resolve(Buffer.allocUnsafe(0)); - break; - case 1: - resolve(buffers[0]); - break; - default: - resolve(Buffer.concat(buffers)); - } + stream.on("end", () => { + switch (buffers.length) { + case 0: + resolve(Buffer.allocUnsafe(0)); + break; + case 1: + resolve(buffers[0]); + break; + default: + resolve(Buffer.concat(buffers)); + } + }); }); - }); } export { extend, stob }; diff --git a/pusher/src/Server/sifrr.server.ts b/pusher/src/Server/sifrr.server.ts index 47fba02c..4ef03721 100644 --- a/pusher/src/Server/sifrr.server.ts +++ b/pusher/src/Server/sifrr.server.ts @@ -1,19 +1,19 @@ -import { parse } from 'query-string'; -import { HttpRequest } from 'uWebSockets.js'; -import App from './server/app'; -import SSLApp from './server/sslapp'; -import * as types from './server/types'; +import { parse } from "query-string"; +import { HttpRequest } from "uWebSockets.js"; +import App from "./server/app"; +import SSLApp from "./server/sslapp"; +import * as types from "./server/types"; const getQuery = (req: HttpRequest) => { - return parse(req.getQuery()); + return parse(req.getQuery()); }; export { App, SSLApp, getQuery }; -export * from './server/types'; +export * from "./server/types"; export default { - App, - SSLApp, - getQuery, - ...types + App, + SSLApp, + getQuery, + ...types, }; diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index fbd7a070..2cbac52c 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -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 {GameRoomPolicyTypes} from "_Model/PusherRoom"; +import { GameRoomPolicyTypes } from "_Model/PusherRoom"; export interface AdminApiData { - organizationSlug: string - worldSlug: string - roomSlug: string - mapUrlStart: string - tags: string[] - policy_type: number - userUuid: string - messages?: unknown[], - textures: CharacterTexture[] + organizationSlug: string; + worldSlug: string; + roomSlug: string; + mapUrlStart: string; + tags: string[]; + policy_type: number; + userUuid: string; + messages?: unknown[]; + textures: CharacterTexture[]; } export interface MapDetailsData { - roomSlug: string, - mapUrl: string, - policy_type: GameRoomPolicyTypes, - tags: string[], + roomSlug: string; + mapUrl: string; + policy_type: GameRoomPolicyTypes; + tags: string[]; } export interface AdminBannedData { - is_banned: boolean, - message: string + is_banned: boolean; + message: string; } export interface CharacterTexture { - id: number, - level: number, - url: string, - rights: string + id: number; + level: number; + url: string; + rights: string; } export interface FetchMemberDataByUuidResponse { uuid: string; tags: string[]; - visitCardUrl: string|null; + visitCardUrl: string | null; textures: CharacterTexture[]; messages: unknown[]; anonymous?: boolean; } class AdminApi { - - async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { + async fetchMapDetails( + organizationSlug: string, + worldSlug: string, + roomSlug: string | undefined + ): Promise { 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, - worldSlug + worldSlug, }; if (roomSlug) { params.roomSlug = roomSlug; } - const res = await Axios.get(ADMIN_API_URL + '/api/map', - { - headers: {"Authorization": `${ADMIN_API_TOKEN}`}, - params - } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/map", { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + params, + }); return res.data; } async fetchMemberDataByUuid(uuid: string, roomId: string): Promise { 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', - { params: {uuid, roomId}, headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/room/access", { + params: { uuid, roomId }, + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } async fetchMemberDataByToken(organizationMemberToken: string): Promise { 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. - const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } async fetchCheckUserByToken(organizationMemberToken: string): Promise { 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. - const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/check-user/" + organizationMemberToken, { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } - reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) { - return Axios.post(`${ADMIN_API_URL}/api/report`, { + reportPlayer( + reportedUserUuid: string, + reportedUserComment: string, + reporterUserUuid: string, + reportWorldSlug: string + ) { + return Axios.post( + `${ADMIN_API_URL}/api/report`, + { reportedUserUuid, reportedUserComment, reporterUserUuid, - reportWorldSlug + reportWorldSlug, }, { - headers: {"Authorization": `${ADMIN_API_TOKEN}`} - }); + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + } + ); } - async verifyBanUser(organizationMemberToken: string, ipAddress: string, organization: string, world: string): Promise { + async verifyBanUser( + organizationMemberToken: string, + ipAddress: string, + organization: string, + world: string + ): Promise { 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. - return Axios.get(ADMIN_API_URL + '/api/check-moderate-user/'+organization+'/'+world+'?ipAddress='+ipAddress+'&token='+organizationMemberToken, - {headers: {"Authorization": `${ADMIN_API_TOKEN}`}} + return Axios.get( + ADMIN_API_URL + + "/api/check-moderate-user/" + + organization + + "/" + + world + + "?ipAddress=" + + ipAddress + + "&token=" + + organizationMemberToken, + { headers: { Authorization: `${ADMIN_API_TOKEN}` } } ).then((data) => { return data.data; }); diff --git a/pusher/src/Services/ApiClientRepository.ts b/pusher/src/Services/ApiClientRepository.ts index c1c6bd38..59e181ae 100644 --- a/pusher/src/Services/ApiClientRepository.ts +++ b/pusher/src/Services/ApiClientRepository.ts @@ -1,14 +1,14 @@ /** * A class to get connections to the correct "api" server given a room name. */ -import {RoomManagerClient} from "../Messages/generated/messages_grpc_pb"; -import grpc from 'grpc'; -import crypto from 'crypto'; -import {API_URL} from "../Enum/EnvironmentVariable"; +import { RoomManagerClient } from "../Messages/generated/messages_grpc_pb"; +import grpc from "grpc"; +import crypto from "crypto"; +import { API_URL } from "../Enum/EnvironmentVariable"; import Debug from "debug"; -const debug = Debug('apiClientRespository'); +const debug = Debug("apiClientRespository"); class ApiClientRepository { private roomManagerClients: RoomManagerClient[] = []; @@ -16,23 +16,26 @@ class ApiClientRepository { public constructor(private apiUrls: string[]) {} public async getClient(roomId: string): Promise { - 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; let client = this.roomManagerClients[index]; if (client === undefined) { - this.roomManagerClients[index] = client = new RoomManagerClient(this.apiUrls[index], grpc.credentials.createInsecure()); - debug('Mapping room %s to API server %s', roomId, this.apiUrls[index]) + this.roomManagerClients[index] = client = new RoomManagerClient( + this.apiUrls[index], + grpc.credentials.createInsecure() + ); + debug("Mapping room %s to API server %s", roomId, this.apiUrls[index]); } return Promise.resolve(client); } public async getAllClients(): Promise { - return [await this.getClient('')]; + return [await this.getClient("")]; } } -const apiClientRepository = new ApiClientRepository(API_URL.split(',')); +const apiClientRepository = new ApiClientRepository(API_URL.split(",")); export { apiClientRepository }; diff --git a/pusher/src/Services/ArrayHelper.ts b/pusher/src/Services/ArrayHelper.ts index 67321d1b..8af1da9f 100644 --- a/pusher/src/Services/ArrayHelper.ts +++ b/pusher/src/Services/ArrayHelper.ts @@ -1,3 +1,3 @@ -export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { - return array1.filter(value => array2.includes(value)).length > 0; -} \ No newline at end of file +export const arrayIntersect = (array1: string[], array2: string[]): boolean => { + return array1.filter((value) => array2.includes(value)).length > 0; +}; diff --git a/pusher/src/Services/ClientEventsEmitter.ts b/pusher/src/Services/ClientEventsEmitter.ts index 7b888ef6..0f56d55c 100644 --- a/pusher/src/Services/ClientEventsEmitter.ts +++ b/pusher/src/Services/ClientEventsEmitter.ts @@ -1,7 +1,7 @@ -const EventEmitter = require('events'); +const EventEmitter = require("events"); -const clientJoinEvent = 'clientJoin'; -const clientLeaveEvent = 'clientLeave'; +const clientJoinEvent = "clientJoin"; +const clientLeaveEvent = "clientLeave"; class ClientEventsEmitter extends EventEmitter { emitClientJoin(clientUUid: string, roomId: string): void { @@ -11,7 +11,7 @@ class ClientEventsEmitter extends EventEmitter { emitClientLeave(clientUUid: string, roomId: string): void { this.emit(clientLeaveEvent, clientUUid, roomId); } - + registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void { this.on(clientJoinEvent, callback); } @@ -29,4 +29,4 @@ class ClientEventsEmitter extends EventEmitter { } } -export const clientEventsEmitter = new ClientEventsEmitter(); \ No newline at end of file +export const clientEventsEmitter = new ClientEventsEmitter(); diff --git a/pusher/src/Services/CpuTracker.ts b/pusher/src/Services/CpuTracker.ts index c7d57f3d..3d06ca70 100644 --- a/pusher/src/Services/CpuTracker.ts +++ b/pusher/src/Services/CpuTracker.ts @@ -1,6 +1,6 @@ -import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; +import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable"; -function secNSec2ms(secNSec: Array|number) { +function secNSec2ms(secNSec: Array | number) { if (Array.isArray(secNSec)) { return secNSec[0] * 1000 + secNSec[1] / 1000000; } @@ -12,17 +12,17 @@ class CpuTracker { private overHeating: boolean = false; constructor() { - let time = process.hrtime.bigint() - let usage = process.cpuUsage() + let time = process.hrtime.bigint(); + let usage = process.cpuUsage(); setInterval(() => { const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage) - usage = process.cpuUsage() + const elapUsage = process.cpuUsage(usage); + usage = process.cpuUsage(); const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user) - const elapSystMS = secNSec2ms(elapUsage.system) - this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) + const elapUserMS = secNSec2ms(elapUsage.user); + const elapSystMS = secNSec2ms(elapUsage.system); + this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000); time = elapTime; diff --git a/pusher/src/Services/GaugeManager.ts b/pusher/src/Services/GaugeManager.ts index f8af822b..9780d89d 100644 --- a/pusher/src/Services/GaugeManager.ts +++ b/pusher/src/Services/GaugeManager.ts @@ -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 class GaugeManager { @@ -9,25 +9,25 @@ class GaugeManager { constructor() { this.nbClientsGauge = new Gauge({ - name: 'workadventure_nb_sockets', - help: 'Number of connected sockets', - labelNames: [ ] + name: "workadventure_nb_sockets", + help: "Number of connected sockets", + labelNames: [], }); this.nbClientsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_clients_per_room', - help: 'Number of clients per room', - labelNames: [ 'room' ] + name: "workadventure_nb_clients_per_room", + help: "Number of clients per room", + labelNames: ["room"], }); this.nbGroupsPerRoomCounter = new Counter({ - name: 'workadventure_counter_groups_per_room', - help: 'Counter of groups per room', - labelNames: [ 'room' ] + name: "workadventure_counter_groups_per_room", + help: "Counter of groups per room", + labelNames: ["room"], }); this.nbGroupsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_groups_per_room', - help: 'Number of groups per room', - labelNames: [ 'room' ] + name: "workadventure_nb_groups_per_room", + help: "Number of groups per room", + labelNames: ["room"], }); } @@ -42,13 +42,13 @@ class GaugeManager { } incNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomCounter.inc({ room: roomId }) - this.nbGroupsPerRoomGauge.inc({ room: roomId }) + this.nbGroupsPerRoomCounter.inc({ room: roomId }); + this.nbGroupsPerRoomGauge.inc({ room: roomId }); } - + decNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomGauge.dec({ room: roomId }) + this.nbGroupsPerRoomGauge.dec({ room: roomId }); } } -export const gaugeManager = new GaugeManager(); \ No newline at end of file +export const gaugeManager = new GaugeManager(); diff --git a/pusher/src/Services/IoSocketHelpers.ts b/pusher/src/Services/IoSocketHelpers.ts index e90e0874..2da7c430 100644 --- a/pusher/src/Services/IoSocketHelpers.ts +++ b/pusher/src/Services/IoSocketHelpers.ts @@ -1,6 +1,6 @@ -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; -import {BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; +import { BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage } from "../Messages/generated/messages_pb"; +import { WebSocket } from "uWebSockets.js"; export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -33,4 +33,3 @@ export function emitError(Client: WebSocket, message: string): void { } console.warn(message); } - diff --git a/pusher/src/Services/JWTTokenManager.ts b/pusher/src/Services/JWTTokenManager.ts index 68d5488a..2e82929e 100644 --- a/pusher/src/Services/JWTTokenManager.ts +++ b/pusher/src/Services/JWTTokenManager.ts @@ -1,73 +1,77 @@ -import {ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; -import {uuid} from "uuidv4"; +import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable"; +import { uuid } from "uuidv4"; import Jwt from "jsonwebtoken"; -import {TokenInterface} from "../Controller/AuthenticateController"; -import {adminApi, AdminBannedData} from "../Services/AdminApi"; +import { TokenInterface } from "../Controller/AuthenticateController"; +import { adminApi, AdminBannedData } from "../Services/AdminApi"; class JWTTokenManager { - 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 { - 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") { - throw new Error('Token is expected to be a string'); + if (typeof token !== "string") { + throw new Error("Token is expected to be a string"); } - - if(token === 'test') { + if (token === "test") { if (ALLOW_ARTILLERY) { return uuid(); } 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((resolve, reject) => { - Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { + Jwt.verify(token, SECRET_KEY, {}, (err, tokenDecoded) => { const tokenInterface = tokenDecoded as TokenInterface; if (err) { - console.error('An authentication error happened, invalid JsonWebToken.', err); - reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message)); + console.error("An authentication error happened, invalid JsonWebToken.", err); + reject(new Error("An authentication error happened, invalid JsonWebToken. " + err.message)); return; } if (tokenDecoded === undefined) { - console.error('Empty token found.'); - reject(new Error('Empty token found.')); + console.error("Empty token found."); + reject(new Error("Empty token found.")); return; } //verify token if (!this.isValidToken(tokenInterface)) { - reject(new Error('Authentication error, invalid token structure.')); + reject(new Error("Authentication error, invalid token structure.")); return; } if (ADMIN_API_URL) { //verify user in admin let promise = new Promise((resolve) => resolve()); - if(ipAddress && room) { + if (ipAddress && room) { promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, room); } - promise.then(() => { - adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => { - resolve(tokenInterface.userUuid); - }).catch((err) => { - //anonymous user - if (err.response && err.response.status && err.response.status === 404) { - resolve(tokenInterface.userUuid); - return; - } + promise + .then(() => { + adminApi + .fetchCheckUserByToken(tokenInterface.userUuid) + .then(() => { + resolve(tokenInterface.userUuid); + }) + .catch((err) => { + //anonymous user + if (err.response && err.response.status && err.response.status === 404) { + resolve(tokenInterface.userUuid); + return; + } + reject(err); + }); + }) + .catch((err) => { reject(err); }); - }).catch((err) => { - reject(err); - }); } else { resolve(tokenInterface.userUuid); } @@ -76,30 +80,32 @@ class JWTTokenManager { } private verifyBanUser(userUuid: string, ipAddress: string, room: string): Promise { - const parts = room.split('/'); - if (parts.length < 3 || parts[0] !== '@') { + const parts = room.split("/"); + if (parts.length < 3 || parts[0] !== "@") { return Promise.resolve({ is_banned: false, - message: '' + message: "", }); } const organization = parts[1]; const world = parts[2]; - return adminApi.verifyBanUser(userUuid, ipAddress, organization, world).then((data: AdminBannedData) => { - if (data && data.is_banned) { - throw new Error('User was banned'); - } - return data; - }).catch((err) => { - throw err; - }); + return adminApi + .verifyBanUser(userUuid, ipAddress, organization, world) + .then((data: AdminBannedData) => { + if (data && data.is_banned) { + throw new Error("User was banned"); + } + return data; + }) + .catch((err) => { + throw err; + }); } 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(); diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 319d2b5e..8a0d3673 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -1,5 +1,5 @@ -import {PusherRoom} from "../Model/PusherRoom"; -import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; +import { PusherRoom } from "../Model/PusherRoom"; +import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; import { GroupDeleteMessage, ItemEventMessage, @@ -31,21 +31,21 @@ import { RefreshRoomMessage, EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; -import {adminApi, CharacterTexture} from "./AdminApi"; -import {emitInBatch} from "./IoSocketHelpers"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; +import { adminApi, CharacterTexture } from "./AdminApi"; +import { emitInBatch } from "./IoSocketHelpers"; import Jwt from "jsonwebtoken"; -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {clientEventsEmitter} from "./ClientEventsEmitter"; -import {gaugeManager} from "./GaugeManager"; -import {apiClientRepository} from "./ApiClientRepository"; -import {GroupDescriptor, UserDescriptor, ZoneEventListener} from "_Model/Zone"; +import { JITSI_URL } from "../Enum/EnvironmentVariable"; +import { clientEventsEmitter } from "./ClientEventsEmitter"; +import { gaugeManager } from "./GaugeManager"; +import { apiClientRepository } from "./ApiClientRepository"; +import { GroupDescriptor, UserDescriptor, ZoneEventListener } from "_Model/Zone"; import Debug from "debug"; -import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; -import {WebSocket} from "uWebSockets.js"; +import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; +import { WebSocket } from "uWebSockets.js"; -const debug = Debug('socket'); +const debug = Debug("socket"); interface AdminSocketRoomsList { [index: string]: number; @@ -55,12 +55,11 @@ interface AdminSocketUsersList { } export interface AdminSocketData { - rooms: AdminSocketRoomsList, - users: AdminSocketUsersList, + rooms: AdminSocketRoomsList; + users: AdminSocketUsersList; } export class SocketManager implements ZoneEventListener { - private rooms: Map = new Map(); private sockets: Map = new Map(); @@ -78,47 +77,53 @@ export class SocketManager implements ZoneEventListener { const adminRoomStream = apiClient.adminRoom(); client.adminConnection = adminRoomStream; - adminRoomStream.on('data', (message: ServerToAdminClientMessage) => { - - if (message.hasUserjoinedroom()) { - const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; - if (!client.disconnecting) { - client.send(JSON.stringify({ - type: 'MemberJoin', - data: { - uuid: userJoinedRoomMessage.getUuid(), - name: userJoinedRoomMessage.getName(), - ipAddress: userJoinedRoomMessage.getIpaddress(), - roomId: roomId, - } - })); + adminRoomStream + .on("data", (message: ServerToAdminClientMessage) => { + if (message.hasUserjoinedroom()) { + const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; + if (!client.disconnecting) { + client.send( + JSON.stringify({ + type: "MemberJoin", + data: { + uuid: userJoinedRoomMessage.getUuid(), + name: userJoinedRoomMessage.getName(), + 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) { - client.send(JSON.stringify({ - type: 'MemberLeave', - data: { - uuid: userLeftRoomMessage.getUuid() - } - })); + this.closeWebsocketConnection(client, 1011, "Connection lost to back server"); } - } else { - throw new Error('Unexpected admin message'); - } - }).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) { - 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'); - } - }); + 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(); message.setSubscribetoroom(roomId); @@ -126,14 +131,14 @@ export class SocketManager implements ZoneEventListener { adminRoomStream.write(message); } - leaveAdminRoom(socket : ExAdminSocketInterface) { + leaveAdminRoom(socket: ExAdminSocketInterface) { if (socket.adminConnection) { socket.adminConnection.end(); } } - getAdminSocketDataFor(roomId:string): AdminSocketData { - throw new Error('Not reimplemented yet'); + getAdminSocketDataFor(roomId: string): AdminSocketData { + throw new Error("Not reimplemented yet"); /*const data:AdminSocketData = { rooms: {}, users: {}, @@ -153,7 +158,6 @@ export class SocketManager implements ZoneEventListener { async handleJoinRoom(client: ExSocketInterface): Promise { const viewport = client.viewport; try { - const joinRoomMessage = new JoinRoomMessage(); joinRoomMessage.setUseruuid(client.userUuid); joinRoomMessage.setIpaddress(client.IPAddress); @@ -176,46 +180,49 @@ export class SocketManager implements ZoneEventListener { joinRoomMessage.addCharacterlayer(characterLayerMessage); } - - console.log('Calling joinRoom') + console.log("Calling joinRoom"); const apiClient = await apiClientRepository.getClient(client.roomId); const streamToPusher = apiClient.joinRoom(); clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); client.backConnection = streamToPusher; - streamToPusher.on('data', (message: ServerToClientMessage) => { - if (message.hasRoomjoinedmessage()) { - client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); - // TODO: do we need this.sockets anymore? - this.sockets.set(client.userId, client); + streamToPusher + .on("data", (message: ServerToClientMessage) => { + if (message.hasRoomjoinedmessage()) { + client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); + // TODO: do we need this.sockets anymore? + this.sockets.set(client.userId, client); - // If this is the first message sent, send back the viewport. - this.handleViewport(client, viewport); - } - - if (message.hasRefreshroommessage()) { - const refreshMessage:RefreshRoomMessage = message.getRefreshroommessage() as unknown as RefreshRoomMessage; - this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber()) - } + // If this is the first message sent, send back the viewport. + this.handleViewport(client, viewport); + } - // Let's pass data over from the back to the client. - if (!client.disconnecting) { - client.send(message.serializeBinary().buffer, true); - } - }).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. - 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'); - } - }); + 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 (!client.disconnecting) { + client.send(message.serializeBinary().buffer, true); + } + }) + .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. + 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(); 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; //this.leaveRoom(client); //client.close(); @@ -257,15 +264,13 @@ export class SocketManager implements ZoneEventListener { const viewport = userMovesMessage.getViewport(); 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. - this.handleViewport(client, viewport.toObject()) + this.handleViewport(client, viewport.toObject()); } - - onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void { const subMessage = new SubMessage(); subMessage.setEmoteeventmessage(emoteMessage); @@ -299,11 +304,16 @@ export class SocketManager implements ZoneEventListener { try { const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); if (!reportedSocket) { - throw 'reported socket user not found'; + throw "reported socket user not found"; } //TODO report user on admin application - //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]) + //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] + ); } catch (e) { console.error('An error occurred on "handleReportMessage"'); console.error(e); @@ -325,14 +335,14 @@ export class SocketManager implements ZoneEventListener { } private searchClientByIdOrFail(userId: number): ExSocketInterface { - const client: ExSocketInterface|undefined = this.sockets.get(userId); + const client: ExSocketInterface | undefined = this.sockets.get(userId); if (client === undefined) { throw new Error("Could not find user with id " + userId); } return client; } - leaveRoom(socket : ExSocketInterface) { + leaveRoom(socket: ExSocketInterface) { // leave previous room and world try { if (socket.roomId) { @@ -340,15 +350,15 @@ export class SocketManager implements ZoneEventListener { //user leaves room const room: PusherRoom | undefined = this.rooms.get(socket.roomId); if (room) { - debug('Leaving room %s.', socket.roomId); - + debug("Leaving room %s.", socket.roomId); + room.leave(socket); if (room.isEmpty()) { this.rooms.delete(socket.roomId); - debug('Room %s is empty. Deleting.', socket.roomId); + debug("Room %s is empty. Deleting.", socket.roomId); } } 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 //Client.leave(Client.roomId); @@ -356,7 +366,7 @@ export class SocketManager implements ZoneEventListener { //delete Client.roomId; this.sockets.delete(socket.userId); 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 { @@ -368,27 +378,27 @@ export class SocketManager implements ZoneEventListener { async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room - let world = this.rooms.get(roomId) - if(world === undefined){ + let world = this.rooms.get(roomId); + if (world === undefined) { world = new PusherRoom(roomId, this); if (!world.public) { await this.updateRoomWithAdminData(world); } this.rooms.set(roomId, world); } - return Promise.resolve(world) + return Promise.resolve(world); } public async updateRoomWithAdminData(world: PusherRoom): Promise { - 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.policyType = Number(data.policy_type); } 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. - throw 'Client is not an admin!'; + throw "Client is not an admin!"; } const pusherToBackMessage = new PusherToBackMessage(); pusherToBackMessage.setPlayglobalmessage(playglobalmessage); @@ -399,44 +409,48 @@ export class SocketManager implements ZoneEventListener { public getWorlds(): Map { return this.rooms; } - + searchClientByUuid(uuid: string): ExSocketInterface | null { - for(const socket of this.sockets.values()){ - if(socket.userUuid === uuid){ + for (const socket of this.sockets.values()) { + if (socket.userUuid === uuid) { return socket; } } return null; } - public handleQueryJitsiJwtMessage(client: ExSocketInterface, queryJitsiJwtMessage: QueryJitsiJwtMessage) { try { 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. - if (SECRET_JITSI_KEY === '') { - throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); + if (SECRET_JITSI_KEY === "") { + 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 const isAdmin = client.tags.includes(tag); - const jwt = Jwt.sign({ - "aud": "jitsi", - "iss": JITSI_ISS, - "sub": JITSI_URL, - "room": room, - "moderator": isAdmin - }, SECRET_JITSI_KEY, { - expiresIn: '1d', - algorithm: "HS256", - header: - { - "alg": "HS256", - "typ": "JWT" - } - }); + const jwt = Jwt.sign( + { + aud: "jitsi", + iss: JITSI_ISS, + sub: JITSI_URL, + room: room, + moderator: isAdmin, + }, + SECRET_JITSI_KEY, + { + expiresIn: "1d", + algorithm: "HS256", + header: { + alg: "HS256", + typ: "JWT", + }, + } + ); const sendJitsiJwtMessage = new SendJitsiJwtMessage(); sendJitsiJwtMessage.setJitsiroom(room); @@ -447,7 +461,7 @@ export class SocketManager implements ZoneEventListener { client.send(serverToClientMessage.serializeBinary().buffer, true); } 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); backConnection.sendAdminMessage(backAdminMessage, (error) => { 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); backConnection.ban(banMessage, (error) => { 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. */ - static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] { + static mergeCharacterLayersAndCustomTextures( + characterLayers: string[], + memberTextures: CharacterTexture[] + ): CharacterLayer[] { const characterLayerObjs: CharacterLayer[] = []; for (const characterLayer of characterLayers) { - if (characterLayer.startsWith('customCharacterTexture')) { + if (characterLayer.startsWith("customCharacterTexture")) { const customCharacterLayerId: number = +characterLayer.substr(22); for (const memberTexture of memberTextures) { if (memberTexture.id == customCharacterLayerId) { characterLayerObjs.push({ name: characterLayer, - url: memberTexture.url - }) + url: memberTexture.url, + }); break; } } } else { characterLayerObjs.push({ name: characterLayer, - url: undefined - }) + url: undefined, + }); } } return characterLayerObjs; @@ -572,7 +589,7 @@ export class SocketManager implements ZoneEventListener { emitInBatch(listener, subMessage); } - + public emitWorldFullMessage(client: WebSocket) { const errorMessage = new WorldFullMessage(); @@ -594,9 +611,9 @@ export class SocketManager implements ZoneEventListener { private refreshRoomData(roomId: string, versionNumber: number): void { 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; - + this.updateRoomWithAdminData(room); } diff --git a/pusher/yarn.lock b/pusher/yarn.lock index 88a20475..f078bddf 100644 --- a/pusher/yarn.lock +++ b/pusher/yarn.lock @@ -117,6 +117,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" @@ -197,6 +202,14 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv@^6.10.0, ajv@^6.10.2: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" @@ -207,6 +220,11 @@ ajv@^6.10.0, ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-escapes@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" @@ -214,6 +232,13 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.11.0" +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -241,6 +266,13 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -325,6 +357,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -389,7 +426,7 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -475,6 +512,14 @@ chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -515,6 +560,11 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -522,6 +572,14 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -573,11 +631,21 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + colour@~0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" integrity sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g= +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -603,6 +671,17 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -614,6 +693,15 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -667,6 +755,11 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -752,7 +845,14 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -error-ex@^1.2.0: +enquirer@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -877,6 +977,21 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -1066,11 +1181,21 @@ generic-type-guard@^3.2.0: resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1" integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ== +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -1193,6 +1318,11 @@ http-status-codes@*: resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.1.4.tgz#453d99b4bd9424254c4f6a9a3a03715923052798" integrity sha512-MZVIsLKGVOVE1KEnldppe6Ij+vmemMuApDfjhVSLzyYP+td0bREEYyAoIw9yFePoBXManCuBqmiNP5FqJS5Xkg== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1220,6 +1350,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -1232,6 +1370,11 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1402,6 +1545,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -1409,6 +1557,21 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -1467,6 +1630,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1549,6 +1717,45 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +lint-staged@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-11.0.0.tgz#24d0a95aa316ba28e257f5c4613369a75a10c712" + integrity sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw== + dependencies: + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + dedent "^0.7.0" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" + normalize-path "^3.0.0" + please-upgrade-node "^3.2.0" + string-argv "0.3.1" + stringify-object "^3.3.0" + +listr2@^3.8.2: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -1610,6 +1817,24 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + long@~3: version "3.2.0" resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" @@ -1661,6 +1886,11 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.2.3: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1685,6 +1915,14 @@ micromatch@^3.1.10: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -1853,6 +2091,13 @@ npm-packlist@^1.1.6: npm-bundled "^1.0.1" npm-normalize-package-bin "^1.0.1" +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -1903,7 +2148,7 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^5.1.0: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -1952,6 +2197,13 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -1966,6 +2218,16 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -1993,6 +2255,11 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -2007,11 +2274,21 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2029,6 +2306,13 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -2039,6 +2323,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" + integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -2226,6 +2515,13 @@ rxjs@^6.6.0: dependencies: tslib "^1.9.0" +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2253,6 +2549,11 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -2290,12 +2591,24 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -signal-exit@^3.0.0, signal-exit@^3.0.2: +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -2309,6 +2622,24 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -2434,6 +2765,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -2469,6 +2805,15 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string-width@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -2476,6 +2821,15 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -2516,6 +2870,11 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -2582,7 +2941,7 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@^2.3.6: +through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -2698,6 +3057,11 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -2785,6 +3149,13 @@ which@^1.2.9: dependencies: isexe "^2.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -2810,6 +3181,24 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -2837,6 +3226,11 @@ yallist@^3.0.0, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs@^3.10.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..166b84c7 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,7 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +husky@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e"