diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index c596c8ee..3405aa71 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -2,6 +2,7 @@ import type { Game } from "../Phaser/Game/Game"; import { chatVisibilityStore } from "../Stores/ChatStore"; import { errorStore } from "../Stores/ErrorStore"; + import { errorScreenStore } from "../Stores/ErrorScreenStore"; import { loginSceneVisibleStore } from "../Stores/LoginSceneStore"; import { enableCameraSceneVisibilityStore } from "../Stores/MediaStore"; import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore"; @@ -13,11 +14,17 @@ import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte"; import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte"; import ErrorDialog from "./UI/ErrorDialog.svelte"; + import ErrorScreen from "./UI/ErrorScreen.svelte"; + export let game: Game; -{#if $errorStore.length > 0} +{#if $errorScreenStore !== undefined} +
+ +
+{:else if $errorStore.length > 0}
diff --git a/front/src/Components/UI/ErrorScreen.svelte b/front/src/Components/UI/ErrorScreen.svelte new file mode 100644 index 00000000..276484b1 --- /dev/null +++ b/front/src/Components/UI/ErrorScreen.svelte @@ -0,0 +1,158 @@ + + +
+
+ +
+ {#if $errorScreenStore.type !== 'retry'}

{$errorScreenStore.title}

{/if} +

{$errorScreenStore.subtitle}

+ {#if $errorScreenStore.type !== 'retry'}

Code : {$errorScreenStore.code}

{/if} +

{detailsStylized}{#if $errorScreenStore.type === 'retry'}

{/if}

+ {#if ($errorScreenStore.type === 'retry' && $errorScreenStore.canRetryManual) || ($errorScreenStore.type === 'redirect' && (window.history.length > 2 || $errorScreenStore.urlToRedirect))} +
+ + {$errorScreenStore.buttonTitle} +
+ {/if} +
+ +
+ + diff --git a/front/src/Components/images/button-large.png b/front/src/Components/images/button-large.png new file mode 100644 index 00000000..f1d52f50 Binary files /dev/null and b/front/src/Components/images/button-large.png differ diff --git a/front/src/Components/images/cup.png b/front/src/Components/images/cup.png new file mode 100644 index 00000000..2f0e15c3 Binary files /dev/null and b/front/src/Components/images/cup.png differ diff --git a/front/src/Components/images/error.png b/front/src/Components/images/error.png new file mode 100644 index 00000000..a0fbc29b Binary files /dev/null and b/front/src/Components/images/error.png differ diff --git a/front/src/Components/images/external-link.png b/front/src/Components/images/external-link.png new file mode 100644 index 00000000..52fca4ed Binary files /dev/null and b/front/src/Components/images/external-link.png differ diff --git a/front/src/Components/images/logo-min-white.png b/front/src/Components/images/logo-min-white.png new file mode 100644 index 00000000..7e5796b0 Binary files /dev/null and b/front/src/Components/images/logo-min-white.png differ diff --git a/front/src/Components/images/reload.png b/front/src/Components/images/reload.png new file mode 100644 index 00000000..414af00c Binary files /dev/null and b/front/src/Components/images/reload.png differ diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index e385fcc7..7291fa17 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -25,6 +25,7 @@ import { TokenExpiredMessage, WorldConnexionMessage, ErrorMessage as ErrorMessageTsProto, + ErrorV2Message as ErrorV2MessageTsProto, UserMovedMessage as UserMovedMessageTsProto, GroupUpdateMessage as GroupUpdateMessageTsProto, GroupDeleteMessage as GroupDeleteMessageTsProto, @@ -46,6 +47,8 @@ import { Subject } from "rxjs"; import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore"; import { gameManager } from "../Phaser/Game/GameManager"; import { SelectCharacterScene, SelectCharacterSceneName } from "../Phaser/Login/SelectCharacterScene"; +import {errorScreenStore} from "../Stores/ErrorScreenStore"; +import {WAError} from "../Phaser/Reconnecting/WAError"; const manualPingDelay = 20000; @@ -61,6 +64,9 @@ export class RoomConnection implements RoomConnection { private readonly _errorMessageStream = new Subject(); public readonly errorMessageStream = this._errorMessageStream.asObservable(); + private readonly _errorV2MessageStream = new Subject(); + public readonly errorV2MessageStream = this._errorV2MessageStream.asObservable(); + private readonly _roomJoinedMessageStream = new Subject<{ connection: RoomConnection; room: RoomJoinedMessageInterface; @@ -476,6 +482,13 @@ export class RoomConnection implements RoomConnection { console.error("An error occurred server side: " + message.errorMessage.message); break; } + case "errorV2Message": { + this._errorV2MessageStream.next(message.errorV2Message); + if(message.errorV2Message.code !== 'retry') this.closed = true; + console.error("An error occurred server side: " + message.errorV2Message.code); + errorScreenStore.setError(message.errorV2Message as unknown as WAError); + break; + } default: { // Security check: if we forget a "case", the line below will catch the error at compile-time. // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts index 41910f26..42c9f617 100644 --- a/front/src/Phaser/Login/EntryScene.ts +++ b/front/src/Phaser/Login/EntryScene.ts @@ -4,9 +4,8 @@ import { ErrorScene } from "../Reconnecting/ErrorScene"; import { WAError } from "../Reconnecting/WAError"; import { waScaleManager } from "../Services/WaScaleManager"; import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene"; -import LL from "../../i18n/i18n-svelte"; -import { get } from "svelte/store"; import { localeDetector } from "../../i18n/locales"; +import {errorScreenStore} from "../../Stores/ErrorScreenStore"; export const EntrySceneName = "EntryScene"; @@ -47,27 +46,18 @@ export class EntryScene extends Scene { this.scene.start(nextSceneName); }) .catch((err) => { - const $LL = get(LL); - if (err.response && err.response.status == 404) { - ErrorScene.showError( - new WAError( - $LL.error.accessLink.title(), - $LL.error.accessLink.subTitle(), - $LL.error.accessLink.details() - ), - this.scene - ); - } else if (err.response && err.response.status == 403) { - ErrorScene.showError( - new WAError( - $LL.error.connectionRejected.title(), - $LL.error.connectionRejected.subTitle({ - error: err.response.data ? ". \n\r \n\r" + `${err.response.data}` : "", - }), - $LL.error.connectionRejected.details() - ), - this.scene - ); + if (err.response.data?.code) { + errorScreenStore.setError(new WAError( + err.response.data.type, + err.response.data.code, + err.response.data.title, + err.response.data.subtitle, + err.response.data.details, + err.response.data.timeToRetry, + err.response.data.canRetryManual, + err.response.data.urlToRedirect, + err.response.data.buttonTitle + )); } else { ErrorScene.showError(err, this.scene); } diff --git a/front/src/Phaser/Reconnecting/ErrorScene.ts b/front/src/Phaser/Reconnecting/ErrorScene.ts index ea593c45..2e86983e 100644 --- a/front/src/Phaser/Reconnecting/ErrorScene.ts +++ b/front/src/Phaser/Reconnecting/ErrorScene.ts @@ -3,7 +3,6 @@ import Image = Phaser.GameObjects.Image; import Sprite = Phaser.GameObjects.Sprite; import Text = Phaser.GameObjects.Text; import ScenePlugin = Phaser.Scenes.ScenePlugin; -import { WAError } from "./WAError"; import Axios from "axios"; export const ErrorSceneName = "ErrorScene"; @@ -88,12 +87,6 @@ export class ErrorScene extends Phaser.Scene { title: "An error occurred", subTitle: error, }); - } else if (error instanceof WAError) { - scene.start(ErrorSceneName, { - title: error.title, - subTitle: error.subTitle, - message: error.details, - }); } else if (Axios.isAxiosError(error) && error.response) { // Axios HTTP error // client received an error response (5xx, 4xx) diff --git a/front/src/Phaser/Reconnecting/WAError.ts b/front/src/Phaser/Reconnecting/WAError.ts index abc71f6c..84bc95ed 100644 --- a/front/src/Phaser/Reconnecting/WAError.ts +++ b/front/src/Phaser/Reconnecting/WAError.ts @@ -1,26 +1,55 @@ export class WAError extends Error { + private _type: string; + private _code: string; private _title: string; - private _subTitle: string; + private _subtitle: string; private _details: string; + private _timeToRetry:number; + private _canRetryManual: boolean; + private _urlToRedirect: string; + private _buttonTitle: string; - constructor(title: string, subTitle: string, details: string) { - super(title + " - " + subTitle + " - " + details); + constructor(type: string, code: string, title: string, subtitle: string, details: string, timeToRetry: number, canRetryManual: boolean, urlToRedirect: string, buttonTitle: string) { + super(title + " - " + subtitle + " - " + details); + + this._type = type; + this._code = code; this._title = title; - this._subTitle = subTitle; + this._subtitle = subtitle; this._details = details; + this._timeToRetry = timeToRetry; + this._canRetryManual = canRetryManual; + this._urlToRedirect = urlToRedirect; + this._buttonTitle = buttonTitle; // Set the prototype explicitly. Object.setPrototypeOf(this, WAError.prototype); } + get type(): string { + return this._type; + } + get code(): string { + return this._code; + } get title(): string { return this._title; } - - get subTitle(): string { - return this._subTitle; + get subtitle(): string { + return this._subtitle; } - get details(): string { return this._details; } + get timeToRetry(): number { + return this._timeToRetry; + } + get buttonTitle(): string { + return this._buttonTitle; + } + get urlToRedirect(): string { + return this._urlToRedirect; + } + get canRetryManual(): boolean { + return this._canRetryManual; + } } diff --git a/front/src/Stores/ErrorScreenStore.ts b/front/src/Stores/ErrorScreenStore.ts new file mode 100644 index 00000000..dc31b1cf --- /dev/null +++ b/front/src/Stores/ErrorScreenStore.ts @@ -0,0 +1,18 @@ +import {writable} from "svelte/store"; +import {WAError} from "../Phaser/Reconnecting/WAError"; + +/** + * A store that contains one error of type WAError to be displayed. + */ +function createErrorScreenStore() { + const { subscribe, set } = writable(undefined); + + return { + subscribe, + setError: ( + e: WAError + ): void => set(e), + }; +} + +export const errorScreenStore = createErrorScreenStore(); diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index fae82184..973ce8ec 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -218,6 +218,18 @@ message ErrorMessage { string message = 1; } +message ErrorV2Message { + string type = 1; + string code = 2; + string title = 3; + string subtitle = 4; + string details = 5; + int32 timeToRetry = 6; + bool canRetryManual = 7; + string urlToRedirect = 8; + string buttonTitle = 9; +} + message ItemStateMessage { int32 itemId = 1; string stateJson = 2; @@ -329,6 +341,7 @@ message ServerToClientMessage { FollowAbortMessage followAbortMessage = 23; InvalidTextureMessage invalidTextureMessage = 24; GroupUsersUpdateMessage groupUsersUpdateMessage = 25; + ErrorV2Message errorV2Message = 26; } } diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 5a5f857d..eb4af3a1 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -318,6 +318,8 @@ export class AuthenticateController extends BaseHttpController { (async () => { const param = await req.json(); + adminApi.setLocale(req.header('accept-language')); + //todo: what to do if the organizationMemberToken is already used? const organizationMemberToken: string | null = param.organizationMemberToken; const playUri: string | null = param.playUri; diff --git a/pusher/src/Controller/BaseHttpController.ts b/pusher/src/Controller/BaseHttpController.ts index a15f7529..17e08e0a 100644 --- a/pusher/src/Controller/BaseHttpController.ts +++ b/pusher/src/Controller/BaseHttpController.ts @@ -31,12 +31,14 @@ export class BaseHttpController { if (axios.isAxiosError(e) && e.response) { res.status(e.response.status); - res.send( - "An error occurred: " + - e.response.status + - " " + - (e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText) - ); + if(!e.response.data?.code) { + res.send( + "An error occurred: " + + e.response.status + + " " + + (e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText) + ); + } else res.json(e.response.data); return; } else { res.status(500); diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 696abbfa..1d805102 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -69,7 +69,7 @@ interface UpgradeData { interface UpgradeFailedData { rejected: true; - reason: "tokenInvalid" | "textureInvalid" | null; + reason: "tokenInvalid" | "textureInvalid" | "error" | null; message: string; roomId: string; } @@ -236,6 +236,8 @@ export class IoSocketController { const websocketExtensions = req.getHeader("sec-websocket-extensions"); const IPAddress = req.getHeader("x-forwarded-for"); + adminApi.setLocale(req.getHeader('accept-language')); + const roomId = query.roomId; try { if (typeof roomId !== "string") { @@ -311,7 +313,7 @@ export class IoSocketController { ); } catch (err) { if (Axios.isAxiosError(err)) { - if (err?.response?.status == 404) { + if (err?.response?.status == 404 || !err?.response?.data.code) { // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! console.warn( @@ -319,16 +321,18 @@ export class IoSocketController { (userIdentifier || "anonymous") + '". 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. + } else if (err?.response?.data.code) { + //OLD // If we get an HTTP 403, the world is full. We need to broadcast a special error to the client. + //OLD // 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, + reason: "error", + message: err?.response?.data.code, status: err?.response?.status, + error: err?.response?.data, roomId, - }, + } as UpgradeFailedData, websocketKey, websocketProtocol, websocketExtensions, @@ -481,8 +485,8 @@ export class IoSocketController { socketManager.emitTokenExpiredMessage(ws); } else if (ws.reason === "textureInvalid") { socketManager.emitInvalidTextureMessage(ws); - } else if (ws.message === "World is full") { - socketManager.emitWorldFullMessage(ws); + } else if (ws.reason === "error") { + socketManager.emitErrorV2Message(ws, ws.error.type, ws.error.code, ws.error.title, ws.error.subtitle, ws.error.details, ws.error.timeToRetry, ws.error.canRetryManual, ws.error.urlToRedirect, ws.error.buttonTitle); } else { socketManager.emitConnexionErrorMessage(ws, ws.message); } diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index 75ac002c..8a95fc86 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -26,6 +26,11 @@ export const isFetchMemberDataByUuidResponse = z.object({ export type FetchMemberDataByUuidResponse = z.infer; class AdminApi { + private locale: string = 'en'; + setLocale(locale: string){ + //console.info('PUSHER LOCALE SET TO :', locale); + this.locale = locale; + } /** * @var playUri: is url of the room * @var userId: can to be undefined or email or uuid @@ -42,7 +47,7 @@ class AdminApi { }; const res = await Axios.get>(ADMIN_API_URL + "/api/map", { - headers: { Authorization: `${ADMIN_API_TOKEN}` }, + headers: { Authorization: `${ADMIN_API_TOKEN}`, 'Accept-Language': this.locale }, params, }); @@ -80,7 +85,7 @@ class AdminApi { ipAddress, characterLayers, }, - headers: { Authorization: `${ADMIN_API_TOKEN}` }, + headers: { Authorization: `${ADMIN_API_TOKEN}`, 'Accept-Language': this.locale }, paramsSerializer: (p) => { return qs.stringify(p, { arrayFormat: "brackets" }); }, @@ -106,7 +111,7 @@ class AdminApi { //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, { params: { playUri }, - headers: { Authorization: `${ADMIN_API_TOKEN}` }, + headers: { Authorization: `${ADMIN_API_TOKEN}`, 'Accept-Language': this.locale }, }); const adminApiData = isAdminApiData.safeParse(res.data); @@ -138,7 +143,7 @@ class AdminApi { reportWorldSlug, }, { - headers: { Authorization: `${ADMIN_API_TOKEN}` }, + headers: { Authorization: `${ADMIN_API_TOKEN}`, 'Accept-Language': this.locale }, } ); } @@ -157,7 +162,7 @@ class AdminApi { encodeURIComponent(userUuid) + "&roomUrl=" + encodeURIComponent(roomUrl), - { headers: { Authorization: `${ADMIN_API_TOKEN}` } } + { headers: { Authorization: `${ADMIN_API_TOKEN}`, 'Accept-Language': this.locale } } ).then((data) => { return data.data; }); @@ -169,7 +174,7 @@ class AdminApi { } return Axios.get(ADMIN_API_URL + "/api/room/sameWorld" + "?roomUrl=" + encodeURIComponent(roomUrl), { - headers: { Authorization: `${ADMIN_API_TOKEN}` }, + headers: { Authorization: `${ADMIN_API_TOKEN}`, 'Accept-Language': this.locale }, }).then((data) => { return data.data; }); diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 8f547cd2..3b1d8fe5 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -39,7 +39,7 @@ import { WorldFullMessage, PlayerDetailsUpdatedMessage, LockGroupPromptMessage, - InvalidTextureMessage, + InvalidTextureMessage, ErrorV2Message, } from "../Messages/generated/messages_pb"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; @@ -643,6 +643,26 @@ export class SocketManager implements ZoneEventListener { client.send(serverToClientMessage.serializeBinary().buffer, true); } + public emitErrorV2Message(client: compressors.WebSocket, type: string, code: string, title: string, subtitle: string, details: string, timeToRetry: number, canRetryManual: boolean, urlToRedirect: string, buttonTitle: string) { + const errorMessage = new ErrorV2Message(); + errorMessage.setType(type); + errorMessage.setCode(code); + errorMessage.setTitle(title); + errorMessage.setSubtitle(subtitle); + errorMessage.setDetails(details); + errorMessage.setTimetoretry(timeToRetry); + errorMessage.setCanretrymanual(canRetryManual); + errorMessage.setUrltoredirect(urlToRedirect); + errorMessage.setButtontitle(buttonTitle); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setErrorv2message(errorMessage); + + //if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); + //} + } + 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.