Squashed commit of the following:

commit 41748a4036
Merge: 3b1d4d63 4991a70b
Author: grégoire parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 21:38:37 2021 +0200

    Merge pull request #1327 from thecodingmachine/hotFixErrorCardBack

    Fix error generated

commit 4991a70bba
Author: Gregoire Parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 21:34:03 2021 +0200

    Fix error generated

    Don't generate error if file is Invalid

commit 3b1d4d630c
Merge: f52b4598 02e5860e
Author: grégoire parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 21:03:18 2021 +0200

    Merge pull request #1326 from thecodingmachine/HotFixCreateMapFeature

    Hot fix create map feature

commit 02e5860e43
Author: Gregoire Parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 20:59:13 2021 +0200

    HotFix redirect on production domain of WorkAdventure

     - Update domain `ADMIN_URL` by `workadventu.re`

commit f52b459872
Merge: 3d657b4a 3ab069d6
Author: grégoire parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 11:23:16 2021 +0200

    Merge pull request #1324 from thecodingmachine/develop

    Release v1.4.11

commit 3ab069d650
Merge: 2b748138 9d4ffe54
Author: Kharhamel <Kharhamel@users.noreply.github.com>
Date:   Fri Jul 30 15:51:07 2021 +0200

    Merge pull request #1323 from thecodingmachine/openIDPoc

    FIX: bomp the node version of pusher

commit 9d4ffe542c
Author: kharhamel <oognic@gmail.com>
Date:   Fri Jul 30 15:50:30 2021 +0200

    FIX: bomp the node version of pusher

commit 2b7481383f
Merge: 74975ac9 9c803a69
Author: Kharhamel <Kharhamel@users.noreply.github.com>
Date:   Fri Jul 30 15:48:56 2021 +0200

    Merge pull request #1251 from thecodingmachine/openIDPoc

    POC for the openID connect

commit 9c803a69ff
Author: kharhamel <oognic@gmail.com>
Date:   Tue Jul 27 16:37:01 2021 +0200

    FEATURE: users can now login via an openID client

commit 74975ac9d8
Merge: 315fe7ca ebdcf880
Author: Kharhamel <Kharhamel@users.noreply.github.com>
Date:   Fri Jul 30 14:54:33 2021 +0200

    Merge pull request #1322 from thecodingmachine/improveCapacityWarning

    FEATURE: improved the room capacity warning visuals

commit ebdcf8804d
Author: kharhamel <oognic@gmail.com>
Date:   Fri Jul 30 14:08:27 2021 +0200

    added admin link to the warning container

commit 41ac51f291
Author: kharhamel <oognic@gmail.com>
Date:   Thu Jul 29 18:02:36 2021 +0200

    FEATURE: improved the room capacity warning visuals

commit 315fe7ca82
Author: David Négrier <d.negrier@thecodingmachine.com>
Date:   Thu Jul 29 17:49:51 2021 +0200

    Adding a "font-family" property for text objects. (#1311)

    - Tiled displays your system fonts.
    - Computers have different sets of fonts. Therefore, browsers never rely on system fonts
    - Which means if you select a font in Tiled, it is quite unlikely it will render properly in WorkAdventure

    To circumvent this problem, in your text object in Tiled, you can now add an additional property: `font-family`.

    The `font-family` property can contain any "web-font" that can be loaded by your browser.

    This allows us to use the "Press Start 2P" 8px font in text objects, which renders way better than the default "Sans serif" font of your browser.

commit 7ffe564e8e
Author: GRL78 <80678534+GRL78@users.noreply.github.com>
Date:   Thu Jul 29 17:42:16 2021 +0200

    Graphic upgrade of the global message console (#1287)

    * Graphic upgrade of the global message console
    Fix: error if LoginScene doesn't exist

    * Rework graphic of global message console

    * Rework graphic of global message console

    * Remove console.log

commit 2a1af2a131
Author: grégoire parant <g.parant@thecodingmachine.com>
Date:   Thu Jul 29 16:42:31 2021 +0200

    PWA service workers (#1319)

    * PWA services worker

    - [x] Register service worker of PWA to install WorkAdventure application on desktop and mobile
    - [x] Create webpage specifique for PWA
    - [ ] Add register service to save and redirect on a card
    - [ ] Add possibilities to install PWA for one World (with register token if existing)

    * Finish PWA strategy to load last map visited

    * Fix feedback @Kharhamel

    * Fix feedback @Kharhamel
This commit is contained in:
Gregoire Parant 2021-08-02 22:06:24 +02:00
parent 6ac25d344b
commit c177f0a1b3
54 changed files with 1469 additions and 1901 deletions

View file

@ -6,6 +6,7 @@ import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
import { localUserStore } from "./LocalUserStore";
import { CharacterTexture, LocalUser } from "./LocalUser";
import { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker";
class ConnectionManager {
private localUser!: LocalUser;
@ -13,6 +14,9 @@ class ConnectionManager {
private connexionType?: GameConnexionTypes;
private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false;
private authToken: string | null = null;
private serviceWorker?: _ServiceWorker;
get unloading() {
return this._unloading;
@ -24,23 +28,58 @@ class ConnectionManager {
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
});
}
public loadOpenIDScreen() {
localUserStore.setAuthToken(null);
const state = localUserStore.generateState();
const nonce = localUserStore.generateNonce();
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`);
}
public logout() {
localUserStore.setAuthToken(null);
window.location.reload();
}
/**
* Tries to login to the node server and return the starting map url to be loaded
*/
public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType;
if (connexionType === GameConnexionTypes.register) {
let room: Room | null = null;
if (connexionType === GameConnexionTypes.jwt) {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const state = urlParams.get("state");
if (!state || !localUserStore.verifyState(state)) {
throw "Could not validate state!";
}
if (!code) {
throw "No Auth code provided";
}
const nonce = localUserStore.getNonce();
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then(
(res) => res.data
);
localUserStore.setAuthToken(authToken);
this.authToken = authToken;
room = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(room);
} else if (connexionType === GameConnexionTypes.register) {
//@deprecated
const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
(res) => res.data
);
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
this.localUser = new LocalUser(data.userUuid, data.textures);
this.authToken = data.authToken;
localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
const roomUrl = data.roomUrl;
const room = await Room.createRoom(
room = await Room.createRoom(
new URL(
window.location.protocol +
"//" +
@ -51,30 +90,17 @@ class ConnectionManager {
)
);
urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room);
} else if (
connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous ||
connexionType === GameConnexionTypes.empty
) {
let localUser = localUserStore.getLocalUser();
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
this.localUser = localUser;
try {
await this.verifyToken(localUser.jwtToken);
} catch (e) {
// If the token is invalid, let's generate an anonymous one.
console.error("JWT token invalid. Did it expire? Login anonymously instead.");
await this.anonymousLogin();
}
} else {
this.authToken = localUserStore.getAuthToken();
//todo: add here some kind of warning if authToken has expired.
if (!this.authToken) {
await this.anonymousLogin();
}
localUser = localUserStore.getLocalUser();
if (!localUser) {
throw "Error to store local user data";
}
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
let roomPath: string;
if (connexionType === GameConnexionTypes.empty) {
@ -90,44 +116,44 @@ class ConnectionManager {
}
//get detail map for anonymous login and set texture in local storage
const room = await Room.createRoom(new URL(roomPath));
room = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) {
//check if texture was changed
if (localUser.textures.length === 0) {
localUser.textures = room.textures;
if (this.localUser.textures.length === 0) {
this.localUser.textures = room.textures;
} else {
room.textures.forEach((newTexture) => {
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
if (localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return;
}
localUser?.textures.push(newTexture);
this.localUser.textures.push(newTexture);
});
}
this.localUser = localUser;
localUserStore.saveUser(localUser);
localUserStore.saveUser(this.localUser);
}
return Promise.resolve(room);
}
if (room == undefined) {
return Promise.reject(new Error("Invalid URL"));
}
return Promise.reject(new Error("Invalid URL"));
}
private async verifyToken(token: string): Promise<void> {
await Axios.get(`${PUSHER_URL}/verify`, { params: { token } });
this.serviceWorker = new _ServiceWorker();
return Promise.resolve(room);
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
this.localUser = new LocalUser(data.userUuid, []);
this.authToken = data.authToken;
if (!isBenchmark) {
// In benchmark, we don't have a local storage.
localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
}
}
public initBenchmark(): void {
this.localUser = new LocalUser("", "test", []);
this.localUser = new LocalUser("", []);
}
public connectToRoomSocket(
@ -140,7 +166,7 @@ class ConnectionManager {
): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection(
this.localUser.jwtToken,
this.authToken,
roomUrl,
name,
characterLayers,
@ -148,6 +174,7 @@ class ConnectionManager {
viewport,
companion
);
connection.onConnectError((error: object) => {
console.log("An error occurred while connecting to socket server. Retrying");
reject(error);
@ -166,6 +193,9 @@ class ConnectionManager {
});
connection.onConnect((connect: OnConnectInterface) => {
//save last room url connected
localUserStore.setLastRoomUrl(roomUrl);
resolve(connect);
});
}).catch((err) => {

View file

@ -1,10 +1,10 @@
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable";
import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
export interface CharacterTexture {
id: number,
level: number,
url: string,
rights: string
id: number;
level: number;
url: string;
rights: string;
}
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
@ -24,6 +24,5 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
}
export class LocalUser {
constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
}
constructor(public readonly uuid: string, public textures: CharacterTexture[]) {}
}

View file

@ -1,60 +1,65 @@
import {areCharacterLayersValid, isUserNameValid, LocalUser} from "./LocalUser";
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
import { v4 as uuidv4 } from "uuid";
const playerNameKey = 'playerName';
const selectedPlayerKey = 'selectedPlayer';
const customCursorPositionKey = 'customCursorPosition';
const characterLayersKey = 'characterLayers';
const companionKey = 'companion';
const gameQualityKey = 'gameQuality';
const videoQualityKey = 'videoQuality';
const audioPlayerVolumeKey = 'audioVolume';
const audioPlayerMuteKey = 'audioMute';
const helpCameraSettingsShown = 'helpCameraSettingsShown';
const fullscreenKey = 'fullscreen';
const playerNameKey = "playerName";
const selectedPlayerKey = "selectedPlayer";
const customCursorPositionKey = "customCursorPosition";
const characterLayersKey = "characterLayers";
const companionKey = "companion";
const gameQualityKey = "gameQuality";
const videoQualityKey = "videoQuality";
const audioPlayerVolumeKey = "audioVolume";
const audioPlayerMuteKey = "audioMute";
const helpCameraSettingsShown = "helpCameraSettingsShown";
const fullscreenKey = "fullscreen";
const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken";
const state = "state";
const nonce = "nonce";
class LocalUserStore {
saveUser(localUser: LocalUser) {
localStorage.setItem('localUser', JSON.stringify(localUser));
localStorage.setItem("localUser", JSON.stringify(localUser));
}
getLocalUser(): LocalUser|null {
const data = localStorage.getItem('localUser');
getLocalUser(): LocalUser | null {
const data = localStorage.getItem("localUser");
return data ? JSON.parse(data) : null;
}
setName(name:string): void {
setName(name: string): void {
localStorage.setItem(playerNameKey, name);
}
getName(): string|null {
const value = localStorage.getItem(playerNameKey) || '';
getName(): string | null {
const value = localStorage.getItem(playerNameKey) || "";
return isUserNameValid(value) ? value : null;
}
setPlayerCharacterIndex(playerCharacterIndex: number): void {
localStorage.setItem(selectedPlayerKey, ''+playerCharacterIndex);
localStorage.setItem(selectedPlayerKey, "" + playerCharacterIndex);
}
getPlayerCharacterIndex(): number {
return parseInt(localStorage.getItem(selectedPlayerKey) || '');
return parseInt(localStorage.getItem(selectedPlayerKey) || "");
}
setCustomCursorPosition(activeRow:number, selectedLayers: number[]): void {
localStorage.setItem(customCursorPositionKey, JSON.stringify({activeRow, selectedLayers}));
setCustomCursorPosition(activeRow: number, selectedLayers: number[]): void {
localStorage.setItem(customCursorPositionKey, JSON.stringify({ activeRow, selectedLayers }));
}
getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null {
getCustomCursorPosition(): { activeRow: number; selectedLayers: number[] } | null {
return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null");
}
setCharacterLayers(layers: string[]): void {
localStorage.setItem(characterLayersKey, JSON.stringify(layers));
}
getCharacterLayers(): string[]|null {
getCharacterLayers(): string[] | null {
const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null");
return areCharacterLayersValid(value) ? value : null;
}
setCompanion(companion: string|null): void {
setCompanion(companion: string | null): void {
return localStorage.setItem(companionKey, JSON.stringify(companion));
}
getCompanion(): string|null {
getCompanion(): string | null {
const companion = JSON.parse(localStorage.getItem(companionKey) || "null");
if (typeof companion !== "string" || companion === "") {
@ -68,45 +73,82 @@ class LocalUserStore {
}
setGameQualityValue(value: number): void {
localStorage.setItem(gameQualityKey, '' + value);
localStorage.setItem(gameQualityKey, "" + value);
}
getGameQualityValue(): number {
return parseInt(localStorage.getItem(gameQualityKey) || '60');
return parseInt(localStorage.getItem(gameQualityKey) || "60");
}
setVideoQualityValue(value: number): void {
localStorage.setItem(videoQualityKey, '' + value);
localStorage.setItem(videoQualityKey, "" + value);
}
getVideoQualityValue(): number {
return parseInt(localStorage.getItem(videoQualityKey) || '20');
return parseInt(localStorage.getItem(videoQualityKey) || "20");
}
setAudioPlayerVolume(value: number): void {
localStorage.setItem(audioPlayerVolumeKey, '' + value);
localStorage.setItem(audioPlayerVolumeKey, "" + value);
}
getAudioPlayerVolume(): number {
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || '1');
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || "1");
}
setAudioPlayerMuted(value: boolean): void {
localStorage.setItem(audioPlayerMuteKey, value.toString());
}
getAudioPlayerMuted(): boolean {
return localStorage.getItem(audioPlayerMuteKey) === 'true';
return localStorage.getItem(audioPlayerMuteKey) === "true";
}
setHelpCameraSettingsShown(): void {
localStorage.setItem(helpCameraSettingsShown, '1');
localStorage.setItem(helpCameraSettingsShown, "1");
}
getHelpCameraSettingsShown(): boolean {
return localStorage.getItem(helpCameraSettingsShown) === '1';
return localStorage.getItem(helpCameraSettingsShown) === "1";
}
setFullscreen(value: boolean): void {
localStorage.setItem(fullscreenKey, value.toString());
}
getFullscreen(): boolean {
return localStorage.getItem(fullscreenKey) === 'true';
return localStorage.getItem(fullscreenKey) === "true";
}
setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString());
}
getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? "";
}
setAuthToken(value: string | null) {
value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken);
}
getAuthToken(): string | null {
return localStorage.getItem(authToken);
}
generateState(): string {
const newState = uuidv4();
localStorage.setItem(state, newState);
return newState;
}
verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state);
localStorage.removeItem(state);
return oldValue === value;
}
generateNonce(): string {
const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce);
return newNonce;
}
getNonce(): string | null {
const oldValue = localStorage.getItem(nonce);
localStorage.removeItem(nonce);
return oldValue;
}
}

View file

@ -32,7 +32,8 @@ import {
EmotePromptMessage,
SendUserMessage,
BanUserMessage,
VariableMessage, ErrorMessage,
VariableMessage,
ErrorMessage,
} from "../Messages/generated/messages_pb";
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
@ -54,9 +55,9 @@ import {
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import { adminMessagesService } from "./AdminMessagesService";
import { worldFullMessageStream } from "./WorldFullMessageStream";
import { worldFullWarningStream } from "./WorldFullWarningStream";
import { connectionManager } from "./ConnectionManager";
import { emoteEventStream } from "./EmoteEventStream";
import { warningContainerStore } from "../Stores/MenuStore";
const manualPingDelay = 20000;
@ -75,7 +76,7 @@ export class RoomConnection implements RoomConnection {
/**
*
* @param token A JWT token containing the UUID of the user
* @param token A JWT token containing the email of the user
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
*/
public constructor(
@ -167,7 +168,7 @@ export class RoomConnection implements RoomConnection {
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else if (subMessage.hasErrormessage()) {
const errorMessage = subMessage.getErrormessage() as ErrorMessage;
console.error('An error occurred server side: '+errorMessage.getMessage());
console.error("An error occurred server side: " + errorMessage.getMessage());
} else if (subMessage.hasVariablemessage()) {
event = EventMessage.SET_VARIABLE;
payload = subMessage.getVariablemessage();
@ -192,7 +193,14 @@ export class RoomConnection implements RoomConnection {
try {
variables.set(variable.getName(), JSON.parse(variable.getValue()));
} catch (e) {
console.error('Unable to unserialize value received from server for variable "'+variable.getName()+'". Value received: "'+variable.getValue()+'". Error: ', e);
console.error(
'Unable to unserialize value received from server for variable "' +
variable.getName() +
'". Value received: "' +
variable.getValue() +
'". Error: ',
e
);
}
}
@ -209,6 +217,9 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage();
this.closed = true;
} else if (message.hasTokenexpiredmessage()) {
connectionManager.loadOpenIDScreen();
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
} else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
this.closed = true;
@ -236,7 +247,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasBanusermessage()) {
adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
} else if (message.hasWorldfullwarningmessage()) {
worldFullWarningStream.onMessage();
warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else {
@ -659,7 +670,14 @@ export class RoomConnection implements RoomConnection {
try {
value = JSON.parse(serializedValue);
} catch (e) {
console.error('Unable to unserialize value received from server for variable "'+name+'". Value received: "'+serializedValue+'". Error: ', e);
console.error(
'Unable to unserialize value received from server for variable "' +
name +
'". Value received: "' +
serializedValue +
'". Error: ',
e
);
}
}
callback(name, value);

View file

@ -1,14 +0,0 @@
import {Subject} from "rxjs";
class WorldFullWarningStream {
private _stream:Subject<void> = new Subject();
public stream = this._stream.asObservable();
onMessage() {
this._stream.next();
}
}
export const worldFullWarningStream = new WorldFullWarningStream();