Merge branch 'use-tiled-objects' of github.com:thecodingmachine/workadventure into use-tiled-objects

This commit is contained in:
Piotr 'pwh' Hanusiak 2022-04-19 12:47:11 +02:00
commit 9aaf2539bc
43 changed files with 672 additions and 138 deletions

View File

@ -21,6 +21,7 @@ jobs:
working-directory: tests working-directory: tests
- name: Install Playwright - name: Install Playwright
run: npx playwright install --with-deps run: npx playwright install --with-deps
working-directory: tests
- name: 'Setup .env file' - name: 'Setup .env file'
run: cp .env.template .env run: cp .env.template .env
- name: Install messages dependencies - name: Install messages dependencies

View File

@ -11,7 +11,7 @@ In order to create Jitsi meet zones:
* You must create a specific object. * You must create a specific object.
* Object must be of type "`area`" * Object must be of type "`area`"
* In object properties, you MUST add a "`jitsiRoom`" property (of type "`string`"). The value of the property is the name of the room in Jitsi. Note: the name of the room will be "slugified" and prepended with the name of the instance of the map (so that different instances of the map have different rooms) * In object properties, you MUST add a "`jitsiRoom`" property (of type "`string`"). The value of the property is the name of the room in Jitsi. Note: the name of the room will be "slugified" and prepended with a hash of the room URL
* You may also use "jitsiWidth" property (of type "number" between 0 and 100) to control the width of the iframe containing the meeting room. * You may also use "jitsiWidth" property (of type "number" between 0 and 100) to control the width of the iframe containing the meeting room.
You can have this object (i.e. your meeting area) to be selectable as the precise location for your meeting using the [Google Calendar integration for Work Adventure](/integrations/google-calendar). To do so, you must set the `meetingRoomLabel` property. You can provide any name that you would like your meeting room to have (as a string). You can have this object (i.e. your meeting area) to be selectable as the precise location for your meeting using the [Google Calendar integration for Work Adventure](/integrations/google-calendar). To do so, you must set the `meetingRoomLabel` property. You can provide any name that you would like your meeting room to have (as a string).
@ -87,3 +87,15 @@ and not
{.alert.alert-info} {.alert.alert-info}
When you use `jitsiUrl`, the targeted Jitsi instance must be public. You cannot use moderation features or the JWT When you use `jitsiUrl`, the targeted Jitsi instance must be public. You cannot use moderation features or the JWT
tokens authentication with maps configured using the `jitsiUrl` property. tokens authentication with maps configured using the `jitsiUrl` property.
## Full control over the Jitsi room name
By default, the name of the room will be "slugified" and prepended with a hash of the room URL.
This is what you want most of the time. Indeed, different maps with the same Jitsi room name (the same `jitsiRoom` property) will not share the same Jitsi room instance.
However, sometimes, you may actually want to have different WorkAdventure meeting rooms that are actually sharing
the same Jitsi meet meeting room. Or if you are pointing to a custom Jitsi server (using the `jitsiUrl` property),
you may want to point to a specific existing room.
For all those use cases, you can use `jitsiNoPrefix: true`. This will remove the automatic prefixing
of the hash and will give you full control on the Jitsi room name.

View File

@ -44,6 +44,7 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"deep-copy-ts": "^0.5.0", "deep-copy-ts": "^0.5.0",
"easystarjs": "^0.4.4", "easystarjs": "^0.4.4",
"fast-deep-equal": "^3.1.3",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"phaser": "3.55.1", "phaser": "3.55.1",
"phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254", "phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254",

View File

@ -9,5 +9,8 @@
"license": "MIT", "license": "MIT",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
},
"dependencies": {
"rxjs": "^6.6.3"
} }
} }

View File

@ -9,7 +9,7 @@ class AnalyticsClient {
constructor() { constructor() {
if (POSTHOG_API_KEY && POSTHOG_URL) { if (POSTHOG_API_KEY && POSTHOG_URL) {
this.posthogPromise = import("posthog-js").then(({ default: posthog }) => { this.posthogPromise = import("posthog-js").then(({ default: posthog }) => {
posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_URL, disable_cookie: true }); posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_URL });
//the posthog toolbar need a reference in window to be able to work //the posthog toolbar need a reference in window to be able to work
window.posthog = posthog; window.posthog = posthog;
return posthog; return posthog;

View File

@ -1,7 +1,13 @@
<script lang="ts"> <script lang="ts">
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene"; import { SelectCompanionScene, SelectCompanionSceneName } from "../../Phaser/Login/SelectCompanionScene";
import { menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected } from "../../Stores/MenuStore"; import {
menuIconVisiblilityStore,
menuVisiblilityStore,
userIsConnected,
profileAvailable,
getProfileUrl,
} from "../../Stores/MenuStore";
import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore"; import { selectCompanionSceneVisibleStore } from "../../Stores/SelectCompanionStore";
import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene"; import { LoginScene, LoginSceneName } from "../../Phaser/Login/LoginScene";
import { loginSceneVisibleStore } from "../../Stores/LoginSceneStore"; import { loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
@ -9,7 +15,6 @@
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import { PROFILE_URL } from "../../Enum/EnvironmentVariable"; import { PROFILE_URL } from "../../Enum/EnvironmentVariable";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene"; import { EnableCameraScene, EnableCameraSceneName } from "../../Phaser/Login/EnableCameraScene";
import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore"; import { enableCameraSceneVisibilityStore } from "../../Stores/MediaStore";
import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg"; import btnProfileSubMenuCamera from "../images/btn-menu-profile-camera.svg";
@ -47,10 +52,6 @@
return connectionManager.logout(); return connectionManager.logout();
} }
function getProfileUrl() {
return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`;
}
function openEnableCameraScene() { function openEnableCameraScene() {
disableMenuStores(); disableMenuStores();
enableCameraSceneVisibilityStore.showEnableCameraScene(); enableCameraSceneVisibilityStore.showEnableCameraScene();
@ -81,7 +82,7 @@
</div> </div>
<div class="content"> <div class="content">
{#if $userIsConnected} {#if $userIsConnected && $profileAvailable}
<section> <section>
{#if PROFILE_URL != undefined} {#if PROFILE_URL != undefined}
<iframe title="profile" src={getProfileUrl()} /> <iframe title="profile" src={getProfileUrl()} />

View File

@ -290,12 +290,12 @@ class ConnectionManager {
); );
connection.onConnectError((error: object) => { connection.onConnectError((error: object) => {
console.log("An error occurred while connecting to socket server. Retrying"); console.log("onConnectError => An error occurred while connecting to socket server. Retrying");
reject(error); reject(error);
}); });
connection.connectionErrorStream.subscribe((event: CloseEvent) => { connection.connectionErrorStream.subscribe((event: CloseEvent) => {
console.log("An error occurred while connecting to socket server. Retrying"); console.log("connectionErrorStream => An error occurred while connecting to socket server. Retrying");
reject( reject(
new Error( new Error(
"An error occurred while connecting to socket server. Retrying. Code: " + "An error occurred while connecting to socket server. Retrying. Code: " +

View File

@ -15,14 +15,9 @@ export interface RoomRedirect {
export class Room { export class Room {
public readonly id: string; public readonly id: string;
/**
* @deprecated
*/
private readonly isPublic: boolean;
private _authenticationMandatory: boolean = DISABLE_ANONYMOUS; private _authenticationMandatory: boolean = DISABLE_ANONYMOUS;
private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER; private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
private _mapUrl: string | undefined; private _mapUrl: string | undefined;
private instance: string | undefined;
private readonly _search: URLSearchParams; private readonly _search: URLSearchParams;
private _contactPage: string | undefined; private _contactPage: string | undefined;
private _group: string | null = null; private _group: string | null = null;
@ -37,13 +32,6 @@ export class Room {
if (this.id.startsWith("/")) { if (this.id.startsWith("/")) {
this.id = this.id.substr(1); this.id = this.id.substr(1);
} }
if (this.id.startsWith("_/") || this.id.startsWith("*/")) {
this.isPublic = true;
} else if (this.id.startsWith("@/")) {
this.isPublic = false;
} else {
throw new Error("Invalid room ID");
}
this._search = new URLSearchParams(roomUrl.search); this._search = new URLSearchParams(roomUrl.search);
} }
@ -84,8 +72,10 @@ export class Room {
const currentRoom = new Room(baseUrl); const currentRoom = new Room(baseUrl);
let instance: string = "global"; let instance: string = "global";
if (currentRoom.isPublic) { if (currentRoom.id.startsWith("_/") || currentRoom.id.startsWith("*/")) {
instance = currentRoom.getInstance(); const match = /[_*]\/([^/]+)\/.+/.exec(currentRoom.id);
if (!match) throw new Error('Could not extract instance from "' + currentRoom.id + '"');
instance = match[1];
} }
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname; baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
@ -151,31 +141,6 @@ export class Room {
} }
} }
/**
* Instance name is:
* - In a public URL: the second part of the URL ( _/[instance]/map.json)
* - In a private URL: [organizationId/worldId]
*
* @deprecated
*/
public getInstance(): string {
if (this.instance !== undefined) {
return this.instance;
}
if (this.isPublic) {
const match = /[_*]\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1];
return this.instance;
} else {
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1] + "/" + match[2];
return this.instance;
}
}
public isDisconnected(): boolean { public isDisconnected(): boolean {
const alone = this._search.get("alone"); const alone = this._search.get("alone");
if (alone && alone !== "0" && alone.toLowerCase() !== "false") { if (alone && alone !== "0" && alone.toLowerCase() !== "false") {

View File

@ -23,6 +23,7 @@ export const DISPLAY_TERMS_OF_USE = getEnvConfig("DISPLAY_TERMS_OF_USE") == "tru
export const NODE_ENV = getEnvConfig("NODE_ENV") || "development"; export const NODE_ENV = getEnvConfig("NODE_ENV") || "development";
export const CONTACT_URL = getEnvConfig("CONTACT_URL") || undefined; export const CONTACT_URL = getEnvConfig("CONTACT_URL") || undefined;
export const PROFILE_URL = getEnvConfig("PROFILE_URL") || undefined; export const PROFILE_URL = getEnvConfig("PROFILE_URL") || undefined;
export const IDENTITY_URL = getEnvConfig("IDENTITY_URL") || undefined;
export const POSTHOG_API_KEY: string = (getEnvConfig("POSTHOG_API_KEY") as string) || ""; export const POSTHOG_API_KEY: string = (getEnvConfig("POSTHOG_API_KEY") as string) || "";
export const POSTHOG_URL = getEnvConfig("POSTHOG_URL") || undefined; export const POSTHOG_URL = getEnvConfig("POSTHOG_URL") || undefined;
export const DISABLE_ANONYMOUS: boolean = getEnvConfig("DISABLE_ANONYMOUS") === "true"; export const DISABLE_ANONYMOUS: boolean = getEnvConfig("DISABLE_ANONYMOUS") === "true";

View File

@ -16,6 +16,7 @@ export enum GameMapProperties {
JITSI_TRIGGER_MESSAGE = "jitsiTriggerMessage", JITSI_TRIGGER_MESSAGE = "jitsiTriggerMessage",
JITSI_URL = "jitsiUrl", JITSI_URL = "jitsiUrl",
JITSI_WIDTH = "jitsiWidth", JITSI_WIDTH = "jitsiWidth",
JITSI_NO_PREFIX = "jitsiNoPrefix",
NAME = "name", NAME = "name",
OPEN_TAB = "openTab", OPEN_TAB = "openTab",
OPEN_WEBSITE = "openWebsite", OPEN_WEBSITE = "openWebsite",

View File

@ -79,7 +79,11 @@ export class GameMapPropertiesListener {
}); });
} else { } else {
const openJitsiRoomFunction = () => { const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.scene.instance); let addPrefix = true;
if (allProps.get(GameMapProperties.JITSI_NO_PREFIX)) {
addPrefix = false;
}
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.scene.roomUrl, addPrefix);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
if (JITSI_PRIVATE_MODE && !jitsiUrl) { if (JITSI_PRIVATE_MODE && !jitsiUrl) {

View File

@ -177,6 +177,7 @@ export class GameScene extends DirtyScene {
private localVolumeStoreUnsubscriber: Unsubscriber | undefined; private localVolumeStoreUnsubscriber: Unsubscriber | undefined;
private followUsersColorStoreUnsubscribe!: Unsubscriber; private followUsersColorStoreUnsubscribe!: Unsubscriber;
private currentPlayerGroupIdStoreUnsubscribe!: Unsubscriber;
private privacyShutdownStoreUnsubscribe!: Unsubscriber; private privacyShutdownStoreUnsubscribe!: Unsubscriber;
private userIsJitsiDominantSpeakerStoreUnsubscriber!: Unsubscriber; private userIsJitsiDominantSpeakerStoreUnsubscriber!: Unsubscriber;
private jitsiParticipantsCountStoreUnsubscriber!: Unsubscriber; private jitsiParticipantsCountStoreUnsubscriber!: Unsubscriber;
@ -184,7 +185,6 @@ export class GameScene extends DirtyScene {
private biggestAvailableAreaStoreUnsubscribe!: () => void; private biggestAvailableAreaStoreUnsubscribe!: () => void;
MapUrlFile: string; MapUrlFile: string;
roomUrl: string; roomUrl: string;
instance: string;
currentTick!: number; currentTick!: number;
lastSentTick!: number; // The last tick at which a position was sent. lastSentTick!: number; // The last tick at which a position was sent.
@ -221,8 +221,8 @@ export class GameScene extends DirtyScene {
private loader: Loader; private loader: Loader;
private lastCameraEvent: WasCameraUpdatedEvent | undefined; private lastCameraEvent: WasCameraUpdatedEvent | undefined;
private firstCameraUpdateSent: boolean = false; private firstCameraUpdateSent: boolean = false;
private showVoiceIndicatorChangeMessageSent: boolean = false;
private currentPlayerGroupId?: number; private currentPlayerGroupId?: number;
private showVoiceIndicatorChangeMessageSent: boolean = false;
private jitsiDominantSpeaker: boolean = false; private jitsiDominantSpeaker: boolean = false;
private jitsiParticipantsCount: number = 0; private jitsiParticipantsCount: number = 0;
public readonly superLoad: SuperLoaderPlugin; public readonly superLoad: SuperLoaderPlugin;
@ -233,7 +233,6 @@ export class GameScene extends DirtyScene {
}); });
this.Terrains = []; this.Terrains = [];
this.groups = new Map<number, Sprite>(); this.groups = new Map<number, Sprite>();
this.instance = room.getInstance();
this.MapUrlFile = MapUrlFile; this.MapUrlFile = MapUrlFile;
this.roomUrl = room.key; this.roomUrl = room.key;
@ -842,6 +841,10 @@ export class GameScene extends DirtyScene {
this.currentPlayerGroupId = message.groupId; this.currentPlayerGroupId = message.groupId;
}); });
this.connection.groupUsersUpdateMessageStream.subscribe((message) => {
this.currentPlayerGroupId = message.groupId;
});
/** /**
* Triggered when we receive the JWT token to connect to Jitsi * Triggered when we receive the JWT token to connect to Jitsi
*/ */

View File

@ -11,6 +11,7 @@ import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError"; import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { SoundMeter } from "../Phaser/Components/SoundMeter";
import deepEqual from "fast-deep-equal";
/** /**
* A store that contains the camera state requested by the user (on or off). * A store that contains the camera state requested by the user (on or off).
@ -314,10 +315,10 @@ export const mediaStreamConstraintsStore = derived(
currentAudioConstraint = false; currentAudioConstraint = false;
} }
// Let's make the changes only if the new value is different from the old one. // Let's make the changes only if the new value is different from the old one.tile
if ( if (
previousComputedVideoConstraint != currentVideoConstraint || !deepEqual(previousComputedVideoConstraint, currentVideoConstraint) ||
previousComputedAudioConstraint != currentAudioConstraint !deepEqual(previousComputedAudioConstraint, currentAudioConstraint)
) { ) {
previousComputedVideoConstraint = currentVideoConstraint; previousComputedVideoConstraint = currentVideoConstraint;
previousComputedAudioConstraint = currentAudioConstraint; previousComputedAudioConstraint = currentAudioConstraint;

View File

@ -1,17 +1,27 @@
import { get, writable } from "svelte/store"; import { get, writable } from "svelte/store";
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
import { userIsAdminStore } from "./GameStore"; import { userIsAdminStore } from "./GameStore";
import { CONTACT_URL } from "../Enum/EnvironmentVariable"; import { CONTACT_URL, IDENTITY_URL, PROFILE_URL } from "../Enum/EnvironmentVariable";
import { analyticsClient } from "../Administration/AnalyticsClient"; import { analyticsClient } from "../Administration/AnalyticsClient";
import type { Translation } from "../i18n/i18n-types"; import type { Translation } from "../i18n/i18n-types";
import axios from "axios";
import { localUserStore } from "../Connexion/LocalUserStore";
export const menuIconVisiblilityStore = writable(false); export const menuIconVisiblilityStore = writable(false);
export const menuVisiblilityStore = writable(false); export const menuVisiblilityStore = writable(false);
menuVisiblilityStore.subscribe((value) => {
if (value) analyticsClient.openedMenu();
});
export const menuInputFocusStore = writable(false); export const menuInputFocusStore = writable(false);
export const userIsConnected = writable(false); export const userIsConnected = writable(false);
export const profileAvailable = writable(true);
menuVisiblilityStore.subscribe((value) => {
if (value) analyticsClient.openedMenu();
if (userIsConnected && value && IDENTITY_URL != null) {
axios.get(getMeUrl()).catch((err) => {
console.error("menuVisiblilityStore => err => ", err);
profileAvailable.set(false);
});
}
});
let warningContainerTimeout: Timeout | null = null; let warningContainerTimeout: Timeout | null = null;
function createWarningContainerStore() { function createWarningContainerStore() {
@ -173,3 +183,11 @@ export function handleMenuUnregisterEvent(menuName: string) {
subMenusStore.removeScriptingMenu(menuName); subMenusStore.removeScriptingMenu(menuName);
customMenuIframe.delete(menuName); customMenuIframe.delete(menuName);
} }
export function getProfileUrl() {
return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`;
}
export function getMeUrl() {
return IDENTITY_URL + `?token=${localUserStore.getAuthToken()}`;
}

View File

@ -9,4 +9,21 @@ export class StringUtils {
} }
return { x: values[0], y: values[1] }; return { x: values[0], y: values[1] };
} }
/**
* Computes a "short URL" hash of the string passed in parameter.
*/
public static shortHash = function (s: string): string {
let hash = 0;
const strLength = s.length;
if (strLength === 0) {
return "";
}
for (let i = 0; i < strLength; i++) {
const c = s.charCodeAt(i);
hash = (hash << 5) - hash + c;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash).toString(36);
};
} }

View File

@ -5,6 +5,7 @@ import { get } from "svelte/store";
import CancelablePromise from "cancelable-promise"; import CancelablePromise from "cancelable-promise";
import { gameManager } from "../Phaser/Game/GameManager"; import { gameManager } from "../Phaser/Game/GameManager";
import { jitsiParticipantsCountStore, userIsJitsiDominantSpeakerStore } from "../Stores/GameStore"; import { jitsiParticipantsCountStore, userIsJitsiDominantSpeakerStore } from "../Stores/GameStore";
import { StringUtils } from "../Utils/StringUtils";
interface jitsiConfigInterface { interface jitsiConfigInterface {
startWithAudioMuted: boolean; startWithAudioMuted: boolean;
@ -120,7 +121,7 @@ const slugify = (...args: (string | number)[]): string => {
.replace(/[\u0300-\u036f]/g, "") // remove all previously split accents .replace(/[\u0300-\u036f]/g, "") // remove all previously split accents
.toLowerCase() .toLowerCase()
.trim() .trim()
.replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced) .replace(/[^a-z0-9-_ ]/g, "") // remove all chars not letters, numbers, dash, underscores and spaces (to be replaced)
.replace(/\s+/g, "-"); // separator .replace(/\s+/g, "-"); // separator
}; };
@ -135,8 +136,8 @@ class JitsiFactory {
/** /**
* Slugifies the room name and prepends the room name with the instance * Slugifies the room name and prepends the room name with the instance
*/ */
public getRoomName(roomName: string, instance: string): string { public getRoomName(roomName: string, roomId: string, addPrefix: boolean): string {
return slugify(instance.replace("/", "-") + "-" + roomName); return slugify((addPrefix ? StringUtils.shortHash(roomId) + "-" : "") + roomName);
} }
public start( public start(

View File

@ -3,17 +3,18 @@ import type { Translation } from "../i18n-types";
const report: NonNullable<Translation["report"]> = { const report: NonNullable<Translation["report"]> = {
block: { block: {
title: "Blockieren", title: "Blockieren",
content: "Blockiere jede Kommunikation von und zu {userName}. Kann jederzeit rückgängig gemacht werden.", content: "Blockiere jegliche Kommunikation mit {userName}. Kann jederzeit rückgängig gemacht werden.",
unblock: "Blockierung für diesen User aufheben", unblock: "Blockierung für diesen Nutzer aufheben",
block: "Blockiere diese User", block: "Blockiere diesen Nutzer",
}, },
title: "Melden", title: "Melden",
content: "Verfasse eine Meldung an die Administratoren dieses Raums. Diese können den User anschließend bannen.", content:
"Verfasse eine Beschwerde an die Administratoren dieses Raums. Diese können den Nutzer anschließend bannen.",
message: { message: {
title: "Deine Nachricht: ", title: "Deine Nachricht: ",
empty: "Bitte einen Text angeben.", empty: "Bitte Text eingeben.",
}, },
submit: "Diesen User melden", submit: "Diesen Nutzer melden",
moderate: { moderate: {
title: "{userName} moderieren", title: "{userName} moderieren",
block: "Blockieren", block: "Blockieren",

View File

@ -0,0 +1,11 @@
import type { Translation } from "../i18n-types";
const audio: NonNullable<Translation["audio"]> = {
manager: {
reduce: "说话时降低音乐音量",
allow: "播放声音",
},
message: "音频消息",
};
export default audio;

View File

@ -0,0 +1,25 @@
import type { Translation } from "../i18n-types";
const camera: NonNullable<Translation["camera"]> = {
enable: {
title: "开启你的摄像头和麦克风",
start: "出发!",
},
help: {
title: "需要摄像头/麦克风权限",
permissionDenied: "拒绝访问",
content: "你必须在浏览器设置里允许摄像头和麦克风访问权限。",
firefoxContent: '如果你不希望Firefox反复要求授权请选中"记住此决定"。',
refresh: "刷新",
continue: "不使用摄像头继续游戏",
screen: {
firefox: "/resources/help-setting-camera-permission/en-US-firefox.png",
chrome: "/resources/help-setting-camera-permission/en-US-firefox.png",
},
},
my: {
silentZone: "安静区",
},
};
export default camera;

View File

@ -0,0 +1,12 @@
import type { Translation } from "../i18n-types";
const chat: NonNullable<Translation["chat"]> = {
intro: "聊天历史:",
enter: "输入消息...",
menu: {
visitCard: "Visit card",
addFriend: "添加朋友",
},
};
export default chat;

View File

@ -0,0 +1,11 @@
import type { Translation } from "../i18n-types";
const companion: NonNullable<Translation["companion"]> = {
select: {
title: "选择你的伙伴",
any: "没有伙伴",
continue: "继续",
},
};
export default companion;

View File

@ -0,0 +1,21 @@
import type { Translation } from "../i18n-types";
const emoji: NonNullable<Translation["emoji"]> = {
search: "搜索 emojis...",
categories: {
recents: "最近的 Emojis",
smileys: "表情",
people: "人物",
animals: "动物和自然",
food: "视频和饮料",
activities: "活动",
travel: "旅行和地点",
objects: "物品",
symbols: "符号",
flags: "旗帜",
custom: "自定义",
},
notFound: "未找到emoji",
};
export default emoji;

View File

@ -0,0 +1,20 @@
import type { Translation } from "../i18n-types";
const error: NonNullable<Translation["error"]> = {
accessLink: {
title: "访问链接错误",
subTitle: "找不到地图。请检查你的访问链接。",
details: "如果你想了解更多信息,你可以联系管理员或联系我们: hello@workadventu.re",
},
connectionRejected: {
title: "连接被拒绝",
subTitle: "你无法加入该世界。请稍后重试 {error}.",
details: "如果你想了解更多信息,你可以联系管理员或联系我们: hello@workadventu.re",
},
connectionRetry: {
unableConnect: "无法链接到 WorkAdventure. 请检查互联网连接。",
},
error: "错误",
};
export default error;

View File

@ -0,0 +1,27 @@
import type { Translation } from "../i18n-types";
const follow: NonNullable<Translation["follow"]> = {
interactStatus: {
following: "跟随 {leader}",
waitingFollowers: "等待跟随者确认",
followed: {
one: "{follower} 正在跟随你",
two: "{firstFollower} 和 {secondFollower} 正在跟随你",
many: "{followers} 和 {lastFollower} 正在跟随你",
},
},
interactMenu: {
title: {
interact: "交互",
follow: "要跟随 {leader} 吗?",
},
stop: {
leader: "要停止领路吗?",
follower: "要停止跟随 {leader} 吗?",
},
yes: "是",
no: "否",
},
};
export default follow;

View File

@ -0,0 +1,36 @@
import en_US from "../en-US";
import type { Translation } from "../i18n-types";
import audio from "./audio";
import camera from "./camera";
import chat from "./chat";
import companion from "./companion";
import woka from "./woka";
import error from "./error";
import follow from "./follow";
import login from "./login";
import menu from "./menu";
import report from "./report";
import warning from "./warning";
import emoji from "./emoji";
import trigger from "./trigger";
const zh_CN: Translation = {
...(en_US as Translation),
language: "中文",
country: "中国",
audio,
camera,
chat,
companion,
woka,
error,
follow,
login,
menu,
report,
warning,
emoji,
trigger,
};
export default zh_CN;

View File

@ -0,0 +1,14 @@
import type { Translation } from "../i18n-types";
const login: NonNullable<Translation["login"]> = {
input: {
name: {
placeholder: "输入你的名字",
empty: "名字为空",
},
},
terms: '点击继续,意味着你同意我们的<a href="https://workadventu.re/terms-of-use" target="_blank">使用协议</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">隐私政策</a> 和 <a href="https://workadventu.re/cookie-policy" target="_blank">Cookie策略</a>.',
continue: "继续",
};
export default login;

View File

@ -0,0 +1,132 @@
import type { Translation } from "../i18n-types";
const menu: NonNullable<Translation["menu"]> = {
title: "菜单",
icon: {
open: {
menu: "打开菜单",
invite: "显示邀请",
register: "注册",
chat: "打开聊天",
},
},
visitCard: {
close: "关闭",
},
profile: {
edit: {
name: "编辑名字",
woka: "编辑 WOKA",
companion: "编辑伙伴",
camera: "摄像头设置",
},
login: "登录",
logout: "登出",
},
settings: {
gameQuality: {
title: "游戏质量",
short: {
high: "高 (120 fps)",
medium: "中 (60 fps)",
small: "低 (40 fps)",
minimum: "最低 (20 fps)",
},
long: {
high: "高视频质量 (120 fps)",
medium: "中视频质量 (60 fps, 推荐)",
small: "低视频质量 (40 fps)",
minimum: "最低视频质量 (20 fps)",
},
},
videoQuality: {
title: "视频质量",
short: {
high: "高 (30 fps)",
medium: "中 (20 fps)",
small: "低 (10 fps)",
minimum: "最低 (5 fps)",
},
long: {
high: "高视频质量 (120 fps)",
medium: "中视频质量 (60 fps, 推荐)",
small: "低视频质量 (40 fps)",
minimum: "最低视频质量 (20 fps)",
},
},
language: {
title: "语言",
},
privacySettings: {
title: "离开模式设置",
explanation:
'当WorkAdventure标签页在后台时, 会切换到"离开模式"。在该模式中,你可以选择自动禁用摄像头 和/或 麦克风 直到标签页显示。',
cameraToggle: "摄像头",
microphoneToggle: "麦克风",
},
save: {
warning: "(保存这些设置会重新加载游戏)",
button: "保存",
},
fullscreen: "全屏",
notifications: "通知",
cowebsiteTrigger: "在打开网页和Jitsi Meet会议前总是询问",
ignoreFollowRequest: "忽略跟随其他用户的请求",
},
invite: {
description: "分享该房间的链接!",
copy: "复制",
share: "分享",
walk_automatically_to_position: "自动走到我的位置",
},
globalMessage: {
text: "文本",
audio: "音频",
warning: "广播到世界的所有房间",
enter: "输入你的消息...",
send: "发送",
},
globalAudio: {
uploadInfo: "上传文件",
error: "未选择文件。发送前必须上传一个文件。",
},
contact: {
gettingStarted: {
title: "开始",
description:
"WorkAdventure使你能够创建一个在线空间与他们自然地交流。这都从创建你自己的空间开始。从我们的团队预制的大量选项中选择一个地图。",
},
createMap: {
title: "创建地图",
description: "你也可以跟随文档中的步骤创建你自己的地图。",
},
},
about: {
mapInfo: "地图信息",
mapLink: "地图链接",
copyrights: {
map: {
title: "地图版权",
empty: "地图创建者未申明地图版权。",
},
tileset: {
title: "tilesets版权",
empty: "地图创建者未申明tilesets版权。这不意味着这些tilesets没有版权。",
},
audio: {
title: "音频文件版权",
empty: "地图创建者未申明音频文件版权。这不意味着这些音频文件没有版权。",
},
},
},
sub: {
profile: "资料",
settings: "设置",
invite: "邀请",
credit: "Credit",
globalMessages: "全局消息",
contact: "联系",
},
};
export default menu;

View File

@ -0,0 +1,25 @@
import type { Translation } from "../i18n-types";
const report: NonNullable<Translation["report"]> = {
block: {
title: "屏蔽",
content: "屏蔽任何来自 {userName} 的通信。该操作是可逆的。",
unblock: "解除屏蔽该用户",
block: "屏蔽该用户",
},
title: "举报",
content: "发送举报信息给这个房间的管理员,他们后续可能禁用该用户。",
message: {
title: "举报信息: ",
empty: "举报信息不能为空.",
},
submit: "举报该用户",
moderate: {
title: "Moderate {userName}",
block: "屏蔽",
report: "举报",
noSelect: "错误:未选择行为。",
},
};
export default report;

View File

@ -0,0 +1,9 @@
import type { Translation } from "../i18n-types";
const trigger: NonNullable<Translation["trigger"]> = {
cowebsite: "按空格键或点击这里打开网页",
jitsiRoom: "按空格键或点击这里进入Jitsi Meet会议",
newTab: "按空格键或点击这里在新标签打开网页",
};
export default trigger;

View File

@ -0,0 +1,18 @@
import type { Translation } from "../i18n-types";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
const upgradeLink = ADMIN_URL + "/pricing";
const warning: NonNullable<Translation["warning"]> = {
title: "警告!",
content: `该世界已接近容量限制!你可以 <a href="${upgradeLink}" target="_blank">点击这里</a> 升级它的容量`,
limit: "该世界已接近容量限制!",
accessDenied: {
camera: "摄像头访问权限被拒绝。点击这里检查你的浏览器权限。",
screenSharing: "屏幕共享权限被拒绝。点击这里检查你的浏览器权限。",
},
importantMessage: "重要消息",
connectionLost: "连接丢失。重新连接中...",
};
export default warning;

View File

@ -0,0 +1,23 @@
import type { Translation } from "../i18n-types";
const woka: NonNullable<Translation["woka"]> = {
customWoka: {
title: "自定义你的WOKA",
navigation: {
return: "返回",
back: "上一个",
finish: "完成",
next: "下一个",
},
},
selectWoka: {
title: "选择你的WOKA",
continue: "继续",
customize: "自定义你的 WOKA",
},
menu: {
businessCard: "Business Card",
},
};
export default woka;

View File

@ -28,6 +28,7 @@ export default defineConfig({
"ADMIN_URL", "ADMIN_URL",
"CONTACT_URL", "CONTACT_URL",
"PROFILE_URL", "PROFILE_URL",
"IDENTITY_URL",
"ICON_URL", "ICON_URL",
"DEBUG_MODE", "DEBUG_MODE",
"STUN_SERVER", "STUN_SERVER",

View File

@ -43,6 +43,11 @@
"type":"string", "type":"string",
"value":"{\"DEFAULT_BACKGROUND\":\"#77ee77\"}" "value":"{\"DEFAULT_BACKGROUND\":\"#77ee77\"}"
}, },
{
"name":"jitsiNoPrefix",
"type":"bool",
"value":true
},
{ {
"name":"jitsiRoom", "name":"jitsiRoom",
"type":"string", "type":"string",
@ -65,7 +70,7 @@
"name":"floorLayer", "name":"floorLayer",
"objects":[ "objects":[
{ {
"height":83.6666666666666, "height":110.891622876526,
"id":1, "id":1,
"name":"", "name":"",
"rotation":0, "rotation":0,
@ -73,14 +78,14 @@
{ {
"fontfamily":"Sans Serif", "fontfamily":"Sans Serif",
"pixelsize":13, "pixelsize":13,
"text":"Test:\nWalk on the carpet and press space\nResult:\nJitsi opens, background in green and audio\/video is muted", "text":"Test:\nWalk on the carpet and press space\nResult:\nJitsi opens, background in green and audio\/video is muted.\nThe name of the room (displayed at the top of Jitsi) is \"Myroom Avec Espace EA\"",
"wrap":true "wrap":true
}, },
"type":"", "type":"",
"visible":true, "visible":true,
"width":315.4375, "width":315.4375,
"x":2.28125, "x":1.48051599382768,
"y":235.166666666667 "y":209.535838407429
}], }],
"opacity":1, "opacity":1,
"type":"objectgroup", "type":"objectgroup",

View File

@ -6,6 +6,7 @@ import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient"; import { openIDClient } from "../Services/OpenIDClient";
import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable"; import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { RegisterData } from "../Messages/JsonMessages/RegisterData"; import { RegisterData } from "../Messages/JsonMessages/RegisterData";
import { adminService } from "../Services/AdminService";
export interface TokenInterface { export interface TokenInterface {
userUuid: string; userUuid: string;
@ -18,6 +19,7 @@ export class AuthenticateController extends BaseHttpController {
this.register(); this.register();
this.anonymLogin(); this.anonymLogin();
this.profileCallback(); this.profileCallback();
this.me();
} }
openIDLogin() { openIDLogin() {
@ -166,10 +168,11 @@ export class AuthenticateController extends BaseHttpController {
//Get user data from Admin Back Office //Get user data from Admin Back Office
//This is very important to create User Local in LocalStorage in WorkAdventure //This is very important to create User Local in LocalStorage in WorkAdventure
const resUserData = await this.getUserByUserIdentifier( const resUserData = await adminService.fetchMemberDataByUuid(
authTokenData.identifier, authTokenData.identifier,
playUri as string, playUri as string,
IPAddress IPAddress,
[]
); );
if (authTokenData.accessToken == undefined) { if (authTokenData.accessToken == undefined) {
@ -178,7 +181,7 @@ export class AuthenticateController extends BaseHttpController {
if (!code && !nonce) { if (!code && !nonce) {
return res.json({ ...resUserData, authToken: token }); return res.json({ ...resUserData, authToken: token });
} }
console.error("Token cannot to be check on OpenId provider"); console.error("Token cannot be checked on OpenId provider");
res.status(500); res.status(500);
res.send("User cannot to be connected on openid provider"); res.send("User cannot to be connected on openid provider");
return; return;
@ -221,7 +224,7 @@ export class AuthenticateController extends BaseHttpController {
//Get user data from Admin Back Office //Get user data from Admin Back Office
//This is very important to create User Local in LocalStorage in WorkAdventure //This is very important to create User Local in LocalStorage in WorkAdventure
const data = await this.getUserByUserIdentifier(email, playUri as string, IPAddress); const data = await adminService.fetchMemberDataByUuid(email, playUri as string, IPAddress, []);
return res.json({ ...data, authToken, username: userInfo?.username, locale: userInfo?.locale }); return res.json({ ...data, authToken, username: userInfo?.username, locale: userInfo?.locale });
} catch (e) { } catch (e) {
@ -253,7 +256,7 @@ export class AuthenticateController extends BaseHttpController {
try { try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false); const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.accessToken == undefined) { if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be logout on Hydra"); throw Error("Token cannot be logout on Hydra");
} }
await openIDClient.logoutUser(authTokenData.accessToken); await openIDClient.logoutUser(authTokenData.accessToken);
} catch (error) { } catch (error) {
@ -410,7 +413,7 @@ export class AuthenticateController extends BaseHttpController {
try { try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false); const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.accessToken == undefined) { if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be check on Hydra"); throw Error("Token cannot be checked on OpenID connect provider");
} }
await openIDClient.checkTokenAuth(authTokenData.accessToken); await openIDClient.checkTokenAuth(authTokenData.accessToken);
@ -431,6 +434,52 @@ export class AuthenticateController extends BaseHttpController {
}); });
} }
/**
* @openapi
* /me:
* get:
* description: ???
* parameters:
* - name: "token"
* in: "query"
* description: "A JWT authentication token ???"
* required: true
* type: "string"
* responses:
* 200:
* description: Data of user connected
*/
me() {
// @ts-ignore
this.app.get("/me", async (req, res): void => {
const { token } = parse(req.path_query);
try {
//verify connected by token
if (token != undefined) {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be checked on Hydra");
}
const me = await openIDClient.checkTokenAuth(authTokenData.accessToken);
//get login profile
res.status(200);
res.json({ ...me });
return;
} catch (error) {
this.castErrorToResponse(error, res);
return;
}
}
} catch (error) {
console.error("me => ERROR", error);
this.castErrorToResponse(error, res);
return;
}
});
}
/** /**
* *
* @param email * @param email

View File

@ -160,12 +160,12 @@ export class MapController extends BaseHttpController {
} }
} }
} }
const mapDetails = isMapDetailsData.safeParse( const mapDetails = isMapDetailsData.parse(
await adminApi.fetchMapDetails(query.playUri as string, userId) await adminApi.fetchMapDetails(query.playUri as string, userId)
); );
if (mapDetails.success && DISABLE_ANONYMOUS) { if (DISABLE_ANONYMOUS) {
mapDetails.data.authenticationMandatory = true; mapDetails.authenticationMandatory = true;
} }
res.json(mapDetails); res.json(mapDetails);

View File

@ -6,6 +6,7 @@ import { AdminApiData, isAdminApiData } from "../Messages/JsonMessages/AdminApiD
import { z } from "zod"; import { z } from "zod";
import { isWokaDetail } from "../Messages/JsonMessages/PlayerTextures"; import { isWokaDetail } from "../Messages/JsonMessages/PlayerTextures";
import qs from "qs"; import qs from "qs";
import { AdminInterface } from "./AdminInterface";
export interface AdminBannedData { export interface AdminBannedData {
is_banned: boolean; is_banned: boolean;
@ -25,7 +26,7 @@ export const isFetchMemberDataByUuidResponse = z.object({
export type FetchMemberDataByUuidResponse = z.infer<typeof isFetchMemberDataByUuidResponse>; export type FetchMemberDataByUuidResponse = z.infer<typeof isFetchMemberDataByUuidResponse>;
class AdminApi { class AdminApi implements AdminInterface {
/** /**
* @var playUri: is url of the room * @var playUri: is url of the room
* @var userId: can to be undefined or email or uuid * @var userId: can to be undefined or email or uuid
@ -65,7 +66,7 @@ class AdminApi {
} }
async fetchMemberDataByUuid( async fetchMemberDataByUuid(
userIdentifier: string | null, userIdentifier: string,
playUri: string, playUri: string,
ipAddress: string, ipAddress: string,
characterLayers: string[] characterLayers: string[]

View File

@ -0,0 +1,10 @@
import { FetchMemberDataByUuidResponse } from "./AdminApi";
export interface AdminInterface {
fetchMemberDataByUuid(
userIdentifier: string,
playUri: string,
ipAddress: string,
characterLayers: string[]
): Promise<FetchMemberDataByUuidResponse>;
}

View File

@ -0,0 +1,5 @@
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { adminApi } from "./AdminApi";
import { localAdmin } from "./LocalAdmin";
export const adminService = ADMIN_API_URL ? adminApi : localAdmin;

View File

@ -0,0 +1,29 @@
import { FetchMemberDataByUuidResponse } from "./AdminApi";
import { AdminInterface } from "./AdminInterface";
/**
* A local class mocking a real admin if no admin is configured.
*/
class LocalAdmin implements AdminInterface {
fetchMemberDataByUuid(
userIdentifier: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
playUri: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ipAddress: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
characterLayers: string[]
): Promise<FetchMemberDataByUuidResponse> {
return Promise.resolve({
email: userIdentifier,
userUuid: userIdentifier,
tags: [],
messages: [],
visitCardUrl: null,
textures: [],
userRoomToken: undefined,
});
}
}
export const localAdmin = new LocalAdmin();

View File

@ -121,7 +121,13 @@ export class SocketManager implements ZoneEventListener {
} }
}) })
.on("end", () => { .on("end", () => {
console.warn("Admin connection lost to back server"); console.warn(
"Admin connection lost to back server '" +
apiClient.getChannel().getTarget() +
"' for room '" +
roomId +
"'"
);
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start.
if (!client.disconnecting) { if (!client.disconnecting) {
this.closeWebsocketConnection(client, 1011, "Admin Connection lost to back server"); this.closeWebsocketConnection(client, 1011, "Admin Connection lost to back server");
@ -129,7 +135,14 @@ export class SocketManager implements ZoneEventListener {
console.log("A user left"); console.log("A user left");
}) })
.on("error", (err: Error) => { .on("error", (err: Error) => {
console.error("Error in connection to back server:", err); console.error(
"Error in connection to back server '" +
apiClient.getChannel().getTarget() +
"' for room '" +
roomId +
"':",
err
);
if (!client.disconnecting) { if (!client.disconnecting) {
this.closeWebsocketConnection(client, 1011, "Error while connecting to back server"); this.closeWebsocketConnection(client, 1011, "Error while connecting to back server");
} }
@ -186,7 +199,7 @@ export class SocketManager implements ZoneEventListener {
joinRoomMessage.addCharacterlayer(characterLayerMessage); joinRoomMessage.addCharacterlayer(characterLayerMessage);
} }
console.log("Calling joinRoom"); console.log("Calling joinRoom '" + client.roomId + "'");
const apiClient = await apiClientRepository.getClient(client.roomId); const apiClient = await apiClientRepository.getClient(client.roomId);
const streamToPusher = apiClient.joinRoom(); const streamToPusher = apiClient.joinRoom();
clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId);
@ -214,7 +227,13 @@ export class SocketManager implements ZoneEventListener {
} }
}) })
.on("end", () => { .on("end", () => {
console.warn("Connection lost to back server"); console.warn(
"Connection lost to back server '" +
apiClient.getChannel().getTarget() +
"' for room '" +
client.roomId +
"'"
);
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start.
if (!client.disconnecting) { if (!client.disconnecting) {
this.closeWebsocketConnection(client, 1011, "Connection lost to back server"); this.closeWebsocketConnection(client, 1011, "Connection lost to back server");
@ -222,7 +241,14 @@ export class SocketManager implements ZoneEventListener {
console.log("A user left"); console.log("A user left");
}) })
.on("error", (err: Error) => { .on("error", (err: Error) => {
console.error("Error in connection to back server:", err); console.error(
"Error in connection to back server '" +
apiClient.getChannel().getTarget() +
"' for room '" +
client.roomId +
"':",
err
);
if (!client.disconnecting) { if (!client.disconnecting) {
this.closeWebsocketConnection(client, 1011, "Error while connecting to back server"); this.closeWebsocketConnection(client, 1011, "Error while connecting to back server");
} }

View File

@ -5,7 +5,7 @@
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.20.0", "@playwright/test": "~1.21.0",
"@types/dockerode": "^3.3.0", "@types/dockerode": "^3.3.0",
"axios": "^0.24.0", "axios": "^0.24.0",
"dockerode": "^3.3.1", "dockerode": "^3.3.1",
@ -854,9 +854,9 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.20.0", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.20.0.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.21.0.tgz",
"integrity": "sha512-UpI5HTcgNLckR0kqXqwNvbcIXtRaDxk+hnO0OBwPSjfbBjRfRgAJ2ClA/b30C5E3UW5dJa17zhsy2qrk66l5cg==", "integrity": "sha512-jvgN3ZeAG6rw85z4u9Rc4uyj6qIaYlq2xrOtS7J2+CDYhzKOttab9ix9ELcvBOCHuQ6wgTfxfJYdh6DRZmQ9hg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "7.16.7", "@babel/code-frame": "7.16.7",
@ -882,13 +882,13 @@
"debug": "4.3.3", "debug": "4.3.3",
"expect": "27.2.5", "expect": "27.2.5",
"jest-matcher-utils": "27.2.5", "jest-matcher-utils": "27.2.5",
"json5": "2.2.0", "json5": "2.2.1",
"mime": "3.0.0", "mime": "3.0.0",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"ms": "2.1.3", "ms": "2.1.3",
"open": "8.4.0", "open": "8.4.0",
"pirates": "4.0.4", "pirates": "4.0.4",
"playwright-core": "1.20.0", "playwright-core": "1.21.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"source-map-support": "0.4.18", "source-map-support": "0.4.18",
"stack-utils": "2.0.5", "stack-utils": "2.0.5",
@ -1007,9 +1007,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/yauzl": { "node_modules/@types/yauzl": {
"version": "2.9.2", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -2071,13 +2071,10 @@
} }
}, },
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true, "dev": true,
"dependencies": {
"minimist": "^1.2.5"
},
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
}, },
@ -2279,9 +2276,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.20.0", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.20.0.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.21.0.tgz",
"integrity": "sha512-d25IRcdooS278Cijlp8J8A5fLQZ+/aY3dKRJvgX5yjXA69N0huIUdnh3xXSgn+LsQ9DCNmB7Ngof3eY630jgdA==", "integrity": "sha512-yDGVs9qaaW6WiefgR7wH1CGt9D6D/X4U3jNpIzH0FjjrrWLCOYQo78Tu3SkW8X+/kWlBpj49iWf3QNSxhYc12Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"colors": "1.4.0", "colors": "1.4.0",
@ -3340,9 +3337,9 @@
} }
}, },
"@playwright/test": { "@playwright/test": {
"version": "1.20.0", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.20.0.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.21.0.tgz",
"integrity": "sha512-UpI5HTcgNLckR0kqXqwNvbcIXtRaDxk+hnO0OBwPSjfbBjRfRgAJ2ClA/b30C5E3UW5dJa17zhsy2qrk66l5cg==", "integrity": "sha512-jvgN3ZeAG6rw85z4u9Rc4uyj6qIaYlq2xrOtS7J2+CDYhzKOttab9ix9ELcvBOCHuQ6wgTfxfJYdh6DRZmQ9hg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "7.16.7", "@babel/code-frame": "7.16.7",
@ -3368,13 +3365,13 @@
"debug": "4.3.3", "debug": "4.3.3",
"expect": "27.2.5", "expect": "27.2.5",
"jest-matcher-utils": "27.2.5", "jest-matcher-utils": "27.2.5",
"json5": "2.2.0", "json5": "2.2.1",
"mime": "3.0.0", "mime": "3.0.0",
"minimatch": "3.0.4", "minimatch": "3.0.4",
"ms": "2.1.3", "ms": "2.1.3",
"open": "8.4.0", "open": "8.4.0",
"pirates": "4.0.4", "pirates": "4.0.4",
"playwright-core": "1.20.0", "playwright-core": "1.21.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"source-map-support": "0.4.18", "source-map-support": "0.4.18",
"stack-utils": "2.0.5", "stack-utils": "2.0.5",
@ -3489,9 +3486,9 @@
"dev": true "dev": true
}, },
"@types/yauzl": { "@types/yauzl": {
"version": "2.9.2", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
@ -4269,13 +4266,10 @@
"dev": true "dev": true
}, },
"json5": { "json5": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true, "dev": true
"requires": {
"minimist": "^1.2.5"
}
}, },
"micromatch": { "micromatch": {
"version": "4.0.4", "version": "4.0.4",
@ -4425,9 +4419,9 @@
} }
}, },
"playwright-core": { "playwright-core": {
"version": "1.20.0", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.20.0.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.21.0.tgz",
"integrity": "sha512-d25IRcdooS278Cijlp8J8A5fLQZ+/aY3dKRJvgX5yjXA69N0huIUdnh3xXSgn+LsQ9DCNmB7Ngof3eY630jgdA==", "integrity": "sha512-yDGVs9qaaW6WiefgR7wH1CGt9D6D/X4U3jNpIzH0FjjrrWLCOYQo78Tu3SkW8X+/kWlBpj49iWf3QNSxhYc12Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"colors": "1.4.0", "colors": "1.4.0",

View File

@ -1,6 +1,6 @@
{ {
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.20.0", "@playwright/test": "~1.21.0",
"@types/dockerode": "^3.3.0", "@types/dockerode": "^3.3.0",
"axios": "^0.24.0", "axios": "^0.24.0",
"dockerode": "^3.3.1", "dockerode": "^3.3.1",

View File

@ -40,17 +40,17 @@
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
//"baseUrl": ".", /* Base directory to resolve non-absolute module names. */ //"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
"paths": { // "paths": {
"_Controller/*": [ // "_Controller/*": [
"src/Controller/*" // "src/Controller/*"
], // ],
"_Model/*": [ // "_Model/*": [
"src/Model/*" // "src/Model/*"
], // ],
"_Enum/*": [ // "_Enum/*": [
"src/Enum/*" // "src/Enum/*"
] // ]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */