Merge branch 'develop' of github.com:thecodingmachine/workadventure into main

This commit is contained in:
_Bastler 2021-08-04 18:02:49 +02:00
commit d046460d76
66 changed files with 1958 additions and 10494 deletions

View File

@ -19,3 +19,6 @@ ACME_EMAIL=
MAX_PER_GROUP=4 MAX_PER_GROUP=4
MAX_USERNAME_LENGTH=8 MAX_USERNAME_LENGTH=8
OPID_CLIENT_ID=
OPID_CLIENT_SECRET=
OPID_CLIENT_ISSUER=

View File

@ -50,6 +50,7 @@ jobs:
run: yarn run build run: yarn run build
env: env:
PUSHER_URL: "//localhost:8080" PUSHER_URL: "//localhost:8080"
ADMIN_URL: "//localhost:80"
working-directory: "front" working-directory: "front"
- name: "Svelte check" - name: "Svelte check"
@ -81,7 +82,7 @@ jobs:
- name: "Setup NodeJS" - name: "Setup NodeJS"
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: '12.x' node-version: '14.x'
- name: Install Protoc - name: Install Protoc
uses: arduino/setup-protoc@v1 uses: arduino/setup-protoc@v1

View File

@ -47,6 +47,7 @@ jobs:
run: yarn run build-typings run: yarn run build-typings
env: env:
PUSHER_URL: "//localhost:8080" PUSHER_URL: "//localhost:8080"
ADMIN_URL: "//localhost:80"
working-directory: "front" working-directory: "front"
# We build the front to generate the typings of iframe_api, then we copy those typings in a separate package. # We build the front to generate the typings of iframe_api, then we copy those typings in a separate package.

View File

@ -1,4 +1,13 @@
## Version 1.4.x-dev ## Version develop
### Updates
- New scripting API features :
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file.
- Rewrote the way authentification works: the auth jwt token can now contains an email instead of an uuid
- Added an OpenId login flow than can be plugged to any OIDC provider.
- You can send a message to all rooms of your world from the console global message (user with tag admin only).
## Version 1.4.11
### Updates ### Updates

View File

@ -272,7 +272,7 @@ const roomManager: IRoomManagerServer = {
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void { sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
// FIXME: we could improve return message by returning a Success|ErrorMessage message // FIXME: we could improve return message by returning a Success|ErrorMessage message
socketManager socketManager
.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()) .sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage(), call.request.getType())
.catch((e) => console.error(e)); .catch((e) => console.error(e));
callback(null, new EmptyMessage()); callback(null, new EmptyMessage());
}, },

View File

@ -27,7 +27,9 @@ class MapFetcher {
}); });
if (!isTiledMap(res.data)) { if (!isTiledMap(res.data)) {
throw new Error("Invalid map format for map " + mapUrl); //TODO fixme
//throw new Error("Invalid map format for map " + mapUrl);
console.error("Invalid map format for map " + mapUrl);
} }
return res.data; return res.data;

View File

@ -761,7 +761,7 @@ export class SocketManager {
} }
} }
async sendAdminRoomMessage(roomId: string, message: string) { async sendAdminRoomMessage(roomId: string, message: string, type: string) {
const room = await this.roomsPromises.get(roomId); const room = await this.roomsPromises.get(roomId);
if (!room) { if (!room) {
//todo: this should cause the http call to return a 500 //todo: this should cause the http call to return a 500
@ -776,7 +776,7 @@ export class SocketManager {
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const sendUserMessage = new SendUserMessage(); const sendUserMessage = new SendUserMessage();
sendUserMessage.setMessage(message); sendUserMessage.setMessage(message);
sendUserMessage.setType("message"); sendUserMessage.setType(type);
const clientMessage = new ServerToClientMessage(); const clientMessage = new ServerToClientMessage();
clientMessage.setSendusermessage(sendUserMessage); clientMessage.setSendusermessage(sendUserMessage);
@ -790,7 +790,7 @@ export class SocketManager {
if (!room) { if (!room) {
//todo: this should cause the http call to return a 500 //todo: this should cause the http call to return a 500
console.error( console.error(
"In sendAdminRoomMessage, could not find room with id '" + "In dispatchWorldFullWarning, could not find room with id '" +
roomId + roomId +
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?" "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
); );

View File

@ -1,58 +1,62 @@
import "jasmine"; import "jasmine";
import {ConnectCallback, DisconnectCallback, GameRoom} from "../src/Model/GameRoom"; import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
import {Point} from "../src/Model/Websocket/MessageUserPosition"; import { Point } from "../src/Model/Websocket/MessageUserPosition";
import {Group} from "../src/Model/Group"; import { Group } from "../src/Model/Group";
import {User, UserSocket} from "_Model/User"; import { User, UserSocket } from "_Model/User";
import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb"; import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
import Direction = PositionMessage.Direction; import Direction = PositionMessage.Direction;
import {EmoteCallback} from "_Model/Zone"; import { EmoteCallback } from "_Model/Zone";
function createMockUser(userId: number): User { function createMockUser(userId: number): User {
return { return {
userId userId,
} as unknown as User; } as unknown as User;
} }
function createMockUserSocket(): UserSocket { function createMockUserSocket(): UserSocket {
return { return {} as unknown as UserSocket;
} as unknown as UserSocket;
} }
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage {
{
const positionMessage = new PositionMessage(); const positionMessage = new PositionMessage();
positionMessage.setX(x); positionMessage.setX(x);
positionMessage.setY(y); positionMessage.setY(y);
positionMessage.setDirection(Direction.DOWN); positionMessage.setDirection(Direction.DOWN);
positionMessage.setMoving(false); positionMessage.setMoving(false);
const joinRoomMessage = new JoinRoomMessage(); const joinRoomMessage = new JoinRoomMessage();
joinRoomMessage.setUseruuid('1'); joinRoomMessage.setUseruuid("1");
joinRoomMessage.setIpaddress('10.0.0.2'); joinRoomMessage.setIpaddress("10.0.0.2");
joinRoomMessage.setName('foo'); joinRoomMessage.setName("foo");
joinRoomMessage.setRoomid('_/global/test.json'); joinRoomMessage.setRoomid("_/global/test.json");
joinRoomMessage.setPositionmessage(positionMessage); joinRoomMessage.setPositionmessage(positionMessage);
return joinRoomMessage; return joinRoomMessage;
} }
const emote: EmoteCallback = (emoteEventMessage, listener): void => {} const emote: EmoteCallback = (emoteEventMessage, listener): void => {};
describe("GameRoom", () => { describe("GameRoom", () => {
it("should connect user1 and user2", async () => { it("should connect user1 and user2", async () => {
let connectCalledNumber: number = 0; let connectCalledNumber: number = 0;
const connect: ConnectCallback = (user: User, group: Group): void => { const connect: ConnectCallback = (user: User, group: Group): void => {
connectCalledNumber++; connectCalledNumber++;
} };
const disconnect: DisconnectCallback = (user: User, group: Group): void => { const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
} const world = await GameRoom.create(
"https://play.workadventu.re/_/global/localhost/test.json",
connect,
disconnect,
160,
160,
() => {},
() => {},
() => {},
emote
);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
const world = await GameRoom.create('https://play.workadventu.re/_/global/localhost/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote); const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100));
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 500, 100));
world.updatePosition(user2, new Point(261, 100)); world.updatePosition(user2, new Point(261, 100));
@ -70,22 +74,30 @@ describe("GameRoom", () => {
let connectCalled: boolean = false; let connectCalled: boolean = false;
const connect: ConnectCallback = (user: User, group: Group): void => { const connect: ConnectCallback = (user: User, group: Group): void => {
connectCalled = true; connectCalled = true;
} };
const disconnect: DisconnectCallback = (user: User, group: Group): void => { const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
} const world = await GameRoom.create(
"https://play.workadventu.re/_/global/localhost/test.json",
connect,
disconnect,
160,
160,
() => {},
() => {},
() => {},
emote
);
const world = await GameRoom.create('https://play.workadventu.re/_/global/localhost/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100)); const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 200, 100));
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 200, 100));
expect(connectCalled).toBe(true); expect(connectCalled).toBe(true);
connectCalled = false; connectCalled = false;
// baz joins at the outer limit of the group // baz joins at the outer limit of the group
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 311, 100)); const user3 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 311, 100));
expect(connectCalled).toBe(false); expect(connectCalled).toBe(false);
@ -99,26 +111,35 @@ describe("GameRoom", () => {
let disconnectCallNumber: number = 0; let disconnectCallNumber: number = 0;
const connect: ConnectCallback = (user: User, group: Group): void => { const connect: ConnectCallback = (user: User, group: Group): void => {
connectCalled = true; connectCalled = true;
} };
const disconnect: DisconnectCallback = (user: User, group: Group): void => { const disconnect: DisconnectCallback = (user: User, group: Group): void => {
disconnectCallNumber++; disconnectCallNumber++;
} };
const world = await GameRoom.create('https://play.workadventu.re/_/global/localhost/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote); const world = await GameRoom.create(
"https://play.workadventu.re/_/global/localhost/test.json",
connect,
disconnect,
160,
160,
() => {},
() => {},
() => {},
emote
);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100)); const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 259, 100)); const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100));
expect(connectCalled).toBe(true); expect(connectCalled).toBe(true);
expect(disconnectCallNumber).toBe(0); expect(disconnectCallNumber).toBe(0);
world.updatePosition(user2, new Point(100+160+160+1, 100)); world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100));
expect(disconnectCallNumber).toBe(2); expect(disconnectCallNumber).toBe(2);
world.updatePosition(user2, new Point(262, 100)); world.updatePosition(user2, new Point(262, 100));
expect(disconnectCallNumber).toBe(2); expect(disconnectCallNumber).toBe(2);
}); });
});
})

View File

@ -13,7 +13,6 @@ RoomConnection.setWebsocketFactory((url: string) => {
}); });
async function startOneUser(): Promise<void> { async function startOneUser(): Promise<void> {
await connectionManager.anonymousLogin(true);
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'], const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
{ {
x: 783, x: 783,
@ -23,7 +22,7 @@ async function startOneUser(): Promise<void> {
bottom: 200, bottom: 200,
left: 500, left: 500,
right: 800 right: 800
}); }, null);
const connection = onConnect.connection; const connection = onConnect.connection;

View File

@ -53,7 +53,7 @@ services:
- "traefik.http.routers.front-ssl.service=front" - "traefik.http.routers.front-ssl.service=front"
pusher: pusher:
image: thecodingmachine/nodejs:12 image: thecodingmachine/nodejs:14
command: yarn dev command: yarn dev
#command: yarn run prod #command: yarn run prod
#command: yarn run profile #command: yarn run profile
@ -66,6 +66,10 @@ services:
API_URL: back:50051 API_URL: back:50051
JITSI_URL: $JITSI_URL JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS JITSI_ISS: $JITSI_ISS
FRONT_URL: http://localhost
OPID_CLIENT_ID: $OPID_CLIENT_ID
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
volumes: volumes:
- ./pusher:/usr/src/app - ./pusher:/usr/src/app
labels: labels:

View File

@ -55,7 +55,7 @@ services:
- "traefik.http.routers.front-ssl.service=front" - "traefik.http.routers.front-ssl.service=front"
pusher: pusher:
image: thecodingmachine/nodejs:12 image: thecodingmachine/nodejs:14
command: yarn dev command: yarn dev
environment: environment:
DEBUG: "socket:*" DEBUG: "socket:*"
@ -66,6 +66,10 @@ services:
API_URL: back:50051 API_URL: back:50051
JITSI_URL: $JITSI_URL JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS JITSI_ISS: $JITSI_ISS
FRONT_URL: http://play.workadventure.localhost
OPID_CLIENT_ID: $OPID_CLIENT_ID
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
volumes: volumes:
- ./pusher:/usr/src/app - ./pusher:/usr/src/app
labels: labels:

View File

@ -163,3 +163,17 @@ WA.room.setTiles([
{x: 9, y: 4, tile: 'blue', layer: 'setTiles'} {x: 9, y: 4, tile: 'blue', layer: 'setTiles'}
]); ]);
``` ```
### Loading a tileset
```
WA.room.loadTileset(url: string): Promise<number>
```
Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset.
You can create a tileset file in Tile Editor.
```javascript
WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]);
})
```

View File

@ -48,6 +48,10 @@
<section> <section>
<button id="toggleFullscreen" class="nes-btn">Toggle fullscreen</button> <button id="toggleFullscreen" class="nes-btn">Toggle fullscreen</button>
</section> </section>
<!-- TODO activate authentication -->
<section hidden>
<button id="oidcLogin">Oauth Login</button>
</section>
<section> <section>
<button id="enableNotification" class="nes-btn">Enable notifications</button> <button id="enableNotification" class="nes-btn">Enable notifications</button>
</section> </section>

View File

@ -1,18 +0,0 @@
<style>
#warningMain {
border-radius: 5px;
height: 100px;
width: 300px;
background-color: red;
text-align: center;
}
#warningMain h2 {
padding: 5px;
}
</style>
<main id="warningMain">
<h2>Warning!</h2>
<p>This world is close to its limit!</p>
</main>

8393
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"@types/mini-css-extract-plugin": "^1.4.3", "@types/mini-css-extract-plugin": "^1.4.3",
"@types/node": "^15.3.0", "@types/node": "^15.3.0",
"@types/quill": "^1.3.7", "@types/quill": "^1.3.7",
"@types/uuidv4": "^5.0.0",
"@types/webpack-dev-server": "^3.11.4", "@types/webpack-dev-server": "^3.11.4",
"@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0", "@typescript-eslint/parser": "^4.23.0",
@ -53,11 +54,13 @@
"phaser3-rex-plugins": "^1.1.42", "phaser3-rex-plugins": "^1.1.42",
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "1.3.6", "quill": "1.3.6",
"quill-delta-to-html": "^0.12.0",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"sanitize-html": "^2.4.0", "sanitize-html": "^2.4.0",
"simple-peer": "^9.11.0", "simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4" "standardized-audio-context": "^25.2.4",
"uuidv4": "^6.2.10"
}, },
"scripts": { "scripts": {
"start": "run-p templater serve svelte-check-watch", "start": "run-p templater serve svelte-check-watch",

View File

@ -1,92 +0,0 @@
import {HtmlUtils} from "./../WebRtc/HtmlUtils";
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection";
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import {soundManager} from "../Phaser/Game/SoundManager";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export class GlobalMessageManager {
constructor(private Connection: RoomConnection) {
this.initialise();
}
initialise(){
//receive signal to show message
this.Connection.receivePlayGlobalMessage((message: PlayGlobalMessageInterface) => {
this.playMessage(message);
});
//receive signal to close message
this.Connection.receiveStopGlobalMessage((messageId: string) => {
this.stopMessage(messageId);
});
//receive signal to close message
this.Connection.receiveTeleportMessage((map: string) => {
console.log('map to teleport user', map);
//TODO teleport user on map
});
}
private playMessage(message : PlayGlobalMessageInterface){
const previousMessage = document.getElementById(this.getHtmlMessageId(message.id));
if(previousMessage){
previousMessage.remove();
}
if(AdminMessageEventTypes.audio === message.type){
this.playAudioMessage(message.id, message.message);
}
if(AdminMessageEventTypes.admin === message.type){
this.playTextMessage(message.id, message.message);
}
}
private playAudioMessage(messageId : string, urlMessage: string) {
soundPlayingStore.playSound(UPLOADER_URL + urlMessage);
}
private playTextMessage(messageId : string, htmlMessage: string){
//add button to clear message
const buttonText = document.createElement('p');
buttonText.id = 'button-clear-message';
buttonText.innerText = 'Clear';
const buttonMainConsole = document.createElement('div');
buttonMainConsole.classList.add('clear');
buttonMainConsole.appendChild(buttonText);
buttonMainConsole.addEventListener('click', () => {
messageContainer.style.top = '-80%';
setTimeout(() => {
messageContainer.remove();
buttonMainConsole.remove();
});
});
//create content message
const messageCotent = document.createElement('div');
messageCotent.innerHTML = htmlMessage;
messageCotent.className = "content-message";
//add message container
const messageContainer = document.createElement('div');
messageContainer.id = this.getHtmlMessageId(messageId);
messageContainer.className = "message-container";
messageContainer.appendChild(messageCotent);
messageContainer.appendChild(buttonMainConsole);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(messageContainer);
}
private stopMessage(messageId: string){
HtmlUtils.removeElementByIdOrFail<HTMLDivElement>(this.getHtmlMessageId(messageId));
}
private getHtmlMessageId(messageId: string) : string{
return `message-${messageId}`;
}
}

View File

@ -1,95 +0,0 @@
import type {TypeMessageInterface} from "./UserMessageManager";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
let modalTimeOut : NodeJS.Timeout;
export class TypeMessageExt implements TypeMessageInterface{
private nbSecond = 0;
private maxNbSecond = 10;
private titleMessage = 'IMPORTANT !';
showMessage(message: string, canDeleteMessage: boolean = true): void {
//delete previous modal
try{
if(modalTimeOut){
clearTimeout(modalTimeOut);
}
const modal = HtmlUtils.getElementByIdOrFail('report-message-user');
modal.remove();
}catch (err){
console.error(err);
}
//create new modal
const div : HTMLDivElement = document.createElement('div');
div.classList.add('modal-report-user');
div.id = 'report-message-user';
div.style.backgroundColor = '#000000e0';
const img : HTMLImageElement = document.createElement('img');
img.src = 'resources/logos/report.svg';
div.appendChild(img);
const title : HTMLParagraphElement = document.createElement('p');
title.id = 'title-report-user';
title.innerText = `${this.titleMessage} (${this.maxNbSecond})`;
div.appendChild(title);
const p : HTMLParagraphElement = document.createElement('p');
p.id = 'body-report-user'
p.innerText = message;
div.appendChild(p);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(div);
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message');
// FIXME: this will fail on iOS
// We should move the sound playing into the GameScene and listen to the event of a report using a store
try {
reportMessageAudio.play();
} catch (e) {
console.error(e);
}
this.nbSecond = this.maxNbSecond;
setTimeout((c) => {
this.forMessage(title, canDeleteMessage);
}, 1000);
}
forMessage(title: HTMLParagraphElement, canDeleteMessage: boolean = true){
this.nbSecond -= 1;
title.innerText = `${this.titleMessage} (${this.nbSecond})`;
if(this.nbSecond > 0){
modalTimeOut = setTimeout(() => {
this.forMessage(title, canDeleteMessage);
}, 1000);
}else {
title.innerText = this.titleMessage;
if (!canDeleteMessage) {
return;
}
const imgCancel: HTMLImageElement = document.createElement('img');
imgCancel.id = 'cancel-report-user';
imgCancel.src = 'resources/logos/close.svg';
const div = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('report-message-user');
div.appendChild(imgCancel);
imgCancel.addEventListener('click', () => {
div.remove();
});
}
}
}
export class Message extends TypeMessageExt {}
export class Ban extends TypeMessageExt {}
export class Banned extends TypeMessageExt {
showMessage(message: string){
super.showMessage(message, false);
}
}

View File

@ -1,43 +1,34 @@
import * as TypeMessages from "./TypeMessage"; import { AdminMessageEventTypes, adminMessagesService } from "../Connexion/AdminMessagesService";
import {Banned} from "./TypeMessage"; import { textMessageContentStore, textMessageVisibleStore } from "../Stores/TypeMessageStore/TextMessageStore";
import {adminMessagesService} from "../Connexion/AdminMessagesService"; import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import { UPLOADER_URL } from "../Enum/EnvironmentVariable";
export interface TypeMessageInterface { import { banMessageContentStore, banMessageVisibleStore } from "../Stores/TypeMessageStore/BanMessageStore";
showMessage(message: string): void;
}
class UserMessageManager { class UserMessageManager {
typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
receiveBannedMessageListener!: Function; receiveBannedMessageListener!: Function;
constructor() { constructor() {
const valueTypeMessageTab = Object.values(TypeMessages);
Object.keys(TypeMessages).forEach((value: string, index: number) => {
const typeMessageInstance: TypeMessageInterface = (new valueTypeMessageTab[index]() as TypeMessageInterface);
this.typeMessages.set(value.toLowerCase(), typeMessageInstance);
});
adminMessagesService.messageStream.subscribe((event) => { adminMessagesService.messageStream.subscribe((event) => {
const typeMessage = this.showMessage(event.type, event.text); textMessageVisibleStore.set(false);
if(typeMessage instanceof Banned) { banMessageVisibleStore.set(false);
if (event.type === AdminMessageEventTypes.admin) {
textMessageContentStore.set(event.text);
textMessageVisibleStore.set(true);
} else if (event.type === AdminMessageEventTypes.audio) {
soundPlayingStore.playSound(UPLOADER_URL + event.text);
} else if (event.type === AdminMessageEventTypes.ban) {
banMessageContentStore.set(event.text);
banMessageVisibleStore.set(true);
} else if (event.type === AdminMessageEventTypes.banned) {
banMessageContentStore.set(event.text);
banMessageVisibleStore.set(true);
this.receiveBannedMessageListener(); this.receiveBannedMessageListener();
} }
}) });
} }
showMessage(type: string, message: string) { setReceiveBanListener(callback: Function) {
const classTypeMessage = this.typeMessages.get(type.toLowerCase());
if (!classTypeMessage) {
console.error('Message unknown');
return;
}
classTypeMessage.showMessage(message);
return classTypeMessage;
}
setReceiveBanListener(callback: Function){
this.receiveBannedMessageListener = callback; this.receiveBannedMessageListener = callback;
} }
} }
export const userMessageManager = new UserMessageManager() export const userMessageManager = new UserMessageManager();

View File

@ -1,5 +1,4 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
import type { GameStateEvent } from "./GameStateEvent";
import type { ButtonClickedEvent } from "./ButtonClickedEvent"; import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent"; import type { ChatEvent } from "./ChatEvent";
import type { ClosePopupEvent } from "./ClosePopupEvent"; import type { ClosePopupEvent } from "./ClosePopupEvent";
@ -10,7 +9,6 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenPopupEvent } from "./OpenPopupEvent"; import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenTabEvent } from "./OpenTabEvent"; import type { OpenTabEvent } from "./OpenTabEvent";
import type { UserInputChatEvent } from "./UserInputChatEvent"; import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { MapDataEvent } from "./MapDataEvent";
import type { LayerEvent } from "./LayerEvent"; import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent"; import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent";
@ -20,9 +18,11 @@ import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent";
import type { SetTilesEvent } from "./SetTilesEvent"; import type { SetTilesEvent } from "./SetTilesEvent";
import type { SetVariableEvent } from "./SetVariableEvent"; import type { SetVariableEvent } from "./SetVariableEvent";
import {isGameStateEvent} from "./GameStateEvent"; import { isGameStateEvent } from "./GameStateEvent";
import {isMapDataEvent} from "./MapDataEvent"; import { isMapDataEvent } from "./MapDataEvent";
import {isSetVariableEvent} from "./SetVariableEvent"; import { isSetVariableEvent } from "./SetVariableEvent";
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
import { isLoadTilesetEvent } from "./LoadTilesetEvent";
export interface TypedMessageEvent<T> extends MessageEvent { export interface TypedMessageEvent<T> extends MessageEvent {
data: T; data: T;
@ -52,6 +52,7 @@ export type IframeEventMap = {
playSound: PlaySoundEvent; playSound: PlaySoundEvent;
stopSound: null; stopSound: null;
getState: undefined; getState: undefined;
loadTileset: LoadTilesetEvent;
registerMenuCommand: MenuItemRegisterEvent; registerMenuCommand: MenuItemRegisterEvent;
setTiles: SetTilesEvent; setTiles: SetTilesEvent;
}; };
@ -83,7 +84,6 @@ export const isIframeResponseEventWrapper = (event: {
type?: string; type?: string;
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string"; }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
/** /**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame. * List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame.
* Types are defined using Type guards that will actually bused to enforce and check types. * Types are defined using Type guards that will actually bused to enforce and check types.
@ -101,22 +101,26 @@ export const iframeQueryMapTypeGuards = {
query: isSetVariableEvent, query: isSetVariableEvent,
answer: tg.isUndefined, answer: tg.isUndefined,
}, },
} loadTileset: {
query: isLoadTilesetEvent,
answer: tg.isNumber,
},
};
type GuardedType<T> = T extends (x: unknown) => x is (infer T) ? T : never; type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards; type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards;
type UnknownToVoid<T> = undefined extends T ? void : T; type UnknownToVoid<T> = undefined extends T ? void : T;
export type IframeQueryMap = { export type IframeQueryMap = {
[key in keyof IframeQueryMapTypeGuardsType]: { [key in keyof IframeQueryMapTypeGuardsType]: {
query: GuardedType<IframeQueryMapTypeGuardsType[key]['query']> query: GuardedType<IframeQueryMapTypeGuardsType[key]["query"]>;
answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]['answer']>> answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]["answer"]>>;
} };
} };
export interface IframeQuery<T extends keyof IframeQueryMap> { export interface IframeQuery<T extends keyof IframeQueryMap> {
type: T; type: T;
data: IframeQueryMap[T]['query']; data: IframeQueryMap[T]["query"];
} }
export interface IframeQueryWrapper<T extends keyof IframeQueryMap> { export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
@ -126,30 +130,34 @@ export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => { export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => {
return type in iframeQueryMapTypeGuards; return type in iframeQueryMapTypeGuards;
} };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => { export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => {
const type = event.type; const type = event.type;
if (typeof type !== 'string') { if (typeof type !== "string") {
return false; return false;
} }
if (!isIframeQueryKey(type)) { if (!isIframeQueryKey(type)) {
return false; return false;
} }
return iframeQueryMapTypeGuards[type].query(event.data); return iframeQueryMapTypeGuards[type].query(event.data);
} };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> => typeof event.id === 'number' && isIframeQuery(event.query); export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> =>
typeof event.id === "number" && isIframeQuery(event.query);
export interface IframeAnswerEvent<T extends keyof IframeQueryMap> { export interface IframeAnswerEvent<T extends keyof IframeQueryMap> {
id: number; id: number;
type: T; type: T;
data: IframeQueryMap[T]['answer']; data: IframeQueryMap[T]["answer"];
} }
export const isIframeAnswerEvent = (event: { type?: string, id?: number }): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === 'string' && typeof event.id === 'number'; export const isIframeAnswerEvent = (event: {
type?: string;
id?: number;
}): event is IframeAnswerEvent<keyof IframeQueryMap> => typeof event.type === "string" && typeof event.id === "number";
export interface IframeErrorAnswerEvent { export interface IframeErrorAnswerEvent {
id: number; id: number;
@ -157,4 +165,9 @@ export interface IframeErrorAnswerEvent {
error: string; error: string;
} }
export const isIframeErrorAnswerEvent = (event: { type?: string, id?: number, error?: string }): event is IframeErrorAnswerEvent => typeof event.type === 'string' && typeof event.id === 'number' && typeof event.error === 'string'; export const isIframeErrorAnswerEvent = (event: {
type?: string;
id?: number;
error?: string;
}): event is IframeErrorAnswerEvent =>
typeof event.type === "string" && typeof event.id === "number" && typeof event.error === "string";

View File

@ -0,0 +1,12 @@
import * as tg from "generic-type-guard";
export const isLoadTilesetEvent = new tg.IsInterface()
.withProperties({
url: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadTilesetEvent = tg.GuardedType<typeof isLoadTilesetEvent>;

View File

@ -1,18 +1,20 @@
import * as tg from "generic-type-guard"; import * as tg from "generic-type-guard";
import {isMenuItemRegisterEvent} from "./ui/MenuItemRegisterEvent"; import { isMenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
export const isSetVariableEvent = export const isSetVariableEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
key: tg.isString, key: tg.isString,
value: tg.isUnknown, value: tg.isUnknown,
}).get(); })
.get();
/** /**
* A message sent from the iFrame to the game to change the value of the property of the layer * A message sent from the iFrame to the game to change the value of the property of the layer
*/ */
export type SetVariableEvent = tg.GuardedType<typeof isSetVariableEvent>; export type SetVariableEvent = tg.GuardedType<typeof isSetVariableEvent>;
export const isSetVariableIframeEvent = export const isSetVariableIframeEvent = new tg.IsInterface()
new tg.IsInterface().withProperties({ .withProperties({
type: tg.isSingletonString("setVariable"), type: tg.isSingletonString("setVariable"),
data: isSetVariableEvent data: isSetVariableEvent,
}).get(); })
.get();

View File

@ -12,7 +12,8 @@ import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent
import { import {
IframeErrorAnswerEvent, IframeErrorAnswerEvent,
IframeEvent, IframeEvent,
IframeEventMap, IframeQueryMap, IframeEventMap,
IframeQueryMap,
IframeResponseEvent, IframeResponseEvent,
IframeResponseEventMap, IframeResponseEventMap,
isIframeEventWrapper, isIframeEventWrapper,
@ -25,16 +26,16 @@ import { isStopSoundEvent, StopSoundEvent } from "./Events/StopSoundEvent";
import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent"; import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"; import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
import type { MapDataEvent } from "./Events/MapDataEvent";
import type { GameStateEvent } from "./Events/GameStateEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent"; import type { SetVariableEvent } from "./Events/SetVariableEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (query: IframeQueryMap[T]["query"], source: MessageEventSource | null) => IframeQueryMap[T]["answer"] | PromiseLike<IframeQueryMap[T]["answer"]>; type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"],
source: MessageEventSource | null
) => IframeQueryMap[T]["answer"] | PromiseLike<IframeQueryMap[T]["answer"]>;
/** /**
* Listens to messages from iframes and turn those messages into easy to use observables. * Listens to messages from iframes and turn those messages into easy to use observables.
@ -115,13 +116,11 @@ class IframeListener {
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false; private sendPlayerMove: boolean = false;
// Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904 // Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904
private answerers: { private answerers: {
[str in keyof IframeQueryMap]?: unknown [str in keyof IframeQueryMap]?: unknown;
} = {}; } = {};
init() { init() {
window.addEventListener( window.addEventListener(
"message", "message",
@ -161,42 +160,56 @@ class IframeListener {
const answerer = this.answerers[query.type] as AnswererCallback<keyof IframeQueryMap> | undefined; const answerer = this.answerers[query.type] as AnswererCallback<keyof IframeQueryMap> | undefined;
if (answerer === undefined) { if (answerer === undefined) {
const errorMsg = 'The iFrame sent a message of type "'+query.type+'" but there is no service configured to answer these messages.'; const errorMsg =
'The iFrame sent a message of type "' +
query.type +
'" but there is no service configured to answer these messages.';
console.error(errorMsg); console.error(errorMsg);
iframe.contentWindow?.postMessage({ iframe.contentWindow?.postMessage(
id: queryId, {
type: query.type, id: queryId,
error: errorMsg type: query.type,
} as IframeErrorAnswerEvent, '*'); error: errorMsg,
} as IframeErrorAnswerEvent,
"*"
);
return; return;
} }
const errorHandler = (reason: unknown) => { const errorHandler = (reason: unknown) => {
console.error('An error occurred while responding to an iFrame query.', reason); console.error("An error occurred while responding to an iFrame query.", reason);
let reasonMsg: string = ''; let reasonMsg: string = "";
if (reason instanceof Error) { if (reason instanceof Error) {
reasonMsg = reason.message; reasonMsg = reason.message;
} else if (typeof reason === 'object') { } else if (typeof reason === "object") {
reasonMsg = reason ? reason.toString() : ''; reasonMsg = reason ? reason.toString() : "";
} else if (typeof reason === 'string') { } else if (typeof reason === "string") {
reasonMsg = reason; reasonMsg = reason;
} }
iframe?.contentWindow?.postMessage({ iframe?.contentWindow?.postMessage(
id: queryId, {
type: query.type, id: queryId,
error: reasonMsg type: query.type,
} as IframeErrorAnswerEvent, '*'); error: reasonMsg,
} as IframeErrorAnswerEvent,
"*"
);
}; };
try { try {
Promise.resolve(answerer(query.data, message.source)).then((value) => { Promise.resolve(answerer(query.data, message.source))
iframe?.contentWindow?.postMessage({ .then((value) => {
id: queryId, iframe?.contentWindow?.postMessage(
type: query.type, {
data: value id: queryId,
}, '*'); type: query.type,
}).catch(errorHandler); data: value,
},
"*"
);
})
.catch(errorHandler);
} catch (reason) { } catch (reason) {
errorHandler(reason); errorHandler(reason);
} }
@ -241,7 +254,7 @@ class IframeListener {
} else if (payload.type === "displayBubble") { } else if (payload.type === "displayBubble") {
this._displayBubbleStream.next(); this._displayBubbleStream.next();
} else if (payload.type === "removeBubble") { } else if (payload.type === "removeBubble") {
this._removeBubbleStream.next(); this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") { } else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true; this.sendPlayerMove = true;
} else if (isMenuItemRegisterIframeEvent(payload)) { } else if (isMenuItemRegisterIframeEvent(payload)) {
@ -405,8 +418,8 @@ class IframeListener {
setVariable(setVariableEvent: SetVariableEvent) { setVariable(setVariableEvent: SetVariableEvent) {
this.postMessage({ this.postMessage({
'type': 'setVariable', type: "setVariable",
'data': setVariableEvent data: setVariableEvent,
}); });
} }
@ -427,7 +440,7 @@ class IframeListener {
* @param key The "type" of the query we are answering * @param key The "type" of the query we are answering
* @param callback * @param callback
*/ */
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: AnswererCallback<T> ): void { public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: AnswererCallback<T>): void {
this.answerers[key] = callback; this.answerers[key] = callback;
} }
@ -439,13 +452,16 @@ class IframeListener {
// Let's dispatch the message to the other iframes // Let's dispatch the message to the other iframes
for (const iframe of this.iframes) { for (const iframe of this.iframes) {
if (iframe.contentWindow !== source) { if (iframe.contentWindow !== source) {
iframe.contentWindow?.postMessage({ iframe.contentWindow?.postMessage(
'type': 'setVariable', {
'data': { type: "setVariable",
key, data: {
value, key,
} value,
}, '*'); },
},
"*"
);
} }
} }
} }

View File

@ -1,4 +1,4 @@
import { Observable, Subject } from "rxjs"; import { Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent"; import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
@ -105,6 +105,14 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
} }
return mapURL; return mapURL;
} }
async loadTileset(url: string): Promise<number> {
return await queryWorkadventure({
type: "loadTileset",
data: {
url: url,
},
});
}
} }
export default new WorkadventureRoomCommands(); export default new WorkadventureRoomCommands();

View File

@ -27,6 +27,12 @@
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore"; import {consoleGlobalMessageManagerVisibleStore} from "../Stores/ConsoleGlobalMessageManagerStore";
import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte"; import ConsoleGlobalMessageManager from "./ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte";
import AdminMessage from "./TypeMessage/BanMessage.svelte";
import TextMessage from "./TypeMessage/TextMessage.svelte";
import {banMessageVisibleStore} from "../Stores/TypeMessageStore/BanMessageStore";
import {textMessageVisibleStore} from "../Stores/TypeMessageStore/TextMessageStore";
import {warningContainerStore} from "../Stores/MenuStore";
import WarningContainer from "./WarningContainer/WarningContainer.svelte";
export let game: Game; export let game: Game;
@ -58,6 +64,16 @@
<EnableCameraScene game={game}></EnableCameraScene> <EnableCameraScene game={game}></EnableCameraScene>
</div> </div>
{/if} {/if}
{#if $banMessageVisibleStore}
<div>
<AdminMessage></AdminMessage>
</div>
{/if}
{#if $textMessageVisibleStore}
<div>
<TextMessage></TextMessage>
</div>
{/if}
{#if $soundPlayingStore} {#if $soundPlayingStore}
<div> <div>
<AudioPlaying url={$soundPlayingStore} /> <AudioPlaying url={$soundPlayingStore} />
@ -91,4 +107,7 @@
{#if $chatVisibilityStore} {#if $chatVisibilityStore}
<Chat></Chat> <Chat></Chat>
{/if} {/if}
{#if $warningContainerStore}
<WarningContainer></WarningContainer>
{/if}
</div> </div>

View File

@ -9,8 +9,8 @@
export let game: Game; export let game: Game;
let inputSendTextActive = true; let inputSendTextActive = true;
let uploadMusicActive = false; let uploadMusicActive = false;
let handleSendText: { sendTextMessage(): void }; let handleSendText: { sendTextMessage(broadcast: boolean): void };
let handleSendAudio: { sendAudioMessage(): Promise<void> }; let handleSendAudio: { sendAudioMessage(broadcast: boolean): Promise<void> };
let broadcastToWorld = false; let broadcastToWorld = false;
function closeConsoleGlobalMessage() { function closeConsoleGlobalMessage() {
@ -35,10 +35,10 @@
function send() { function send() {
if (inputSendTextActive) { if (inputSendTextActive) {
handleSendText.sendTextMessage(); handleSendText.sendTextMessage(broadcastToWorld);
} }
if (uploadMusicActive) { if (uploadMusicActive) {
handleSendAudio.sendAudioMessage(); handleSendAudio.sendAudioMessage(broadcastToWorld);
} }
} }
</script> </script>

View File

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore"; import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import { onMount } from "svelte"; import {onDestroy, onMount} from "svelte";
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import type { GameManager } from "../../Phaser/Game/GameManager"; import type { GameManager } from "../../Phaser/Game/GameManager";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService"; import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import type { Quill } from "quill"; import type { Quill } from "quill";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
//toolbar //toolbar
const toolbarOptions = [ const toolbarOptions = [
@ -34,27 +34,27 @@
export let game: Game; export let game: Game;
export let gameManager: GameManager; export let gameManager: GameManager;
let gameScene = gameManager.getCurrentGameScene(game.findAnyScene()); const gameScene = gameManager.getCurrentGameScene(game.findAnyScene());
let quill: Quill; let quill: Quill;
let INPUT_CONSOLE_MESSAGE: HTMLDivElement; let INPUT_CONSOLE_MESSAGE: HTMLDivElement;
const MESSAGE_TYPE = AdminMessageEventTypes.admin; const MESSAGE_TYPE = AdminMessageEventTypes.admin;
export const handleSending = { export const handleSending = {
sendTextMessage() { sendTextMessage(broadcastToWorld: boolean) {
if (gameScene == undefined) { if (gameScene == undefined) {
return; return;
} }
const text = quill.getText(0, quill.getLength()); const text = JSON.stringify(quill.getContents(0, quill.getLength()));
const GlobalMessage: PlayGlobalMessageInterface = { const textGlobalMessage: PlayGlobalMessageInterface = {
id: "1", // FIXME: use another ID? type: MESSAGE_TYPE,
message: text, content: text,
type: MESSAGE_TYPE broadcastToWorld: broadcastToWorld
}; };
quill.deleteText(0, quill.getLength()); quill.deleteText(0, quill.getLength());
gameScene.connection?.emitGlobalMessage(GlobalMessage); gameScene.connection?.emitGlobalMessage(textGlobalMessage);
disableConsole(); disableConsole();
} }
} }
@ -73,14 +73,13 @@
}, },
}); });
quill.on('selection-change', function (range, oldRange) { consoleGlobalMessageManagerFocusStore.set(true);
if (range === null && oldRange !== null) {
consoleGlobalMessageManagerFocusStore.set(false);
} else if (range !== null && oldRange === null)
consoleGlobalMessageManagerFocusStore.set(true);
});
}); });
onDestroy(() => {
consoleGlobalMessageManagerFocusStore.set(false);
})
function disableConsole() { function disableConsole() {
consoleGlobalMessageManagerVisibleStore.set(false); consoleGlobalMessageManagerVisibleStore.set(false);
consoleGlobalMessageManagerFocusStore.set(false); consoleGlobalMessageManagerFocusStore.set(false);

View File

@ -4,8 +4,8 @@
import type { GameManager } from "../../Phaser/Game/GameManager"; import type { GameManager } from "../../Phaser/Game/GameManager";
import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore"; import { consoleGlobalMessageManagerFocusStore, consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService"; import { AdminMessageEventTypes } from "../../Connexion/AdminMessagesService";
import type { PlayGlobalMessageInterface } from "../../Connexion/ConnexionModels";
import uploadFile from "../images/music-file.svg"; import uploadFile from "../images/music-file.svg";
import type {PlayGlobalMessageInterface} from "../../Connexion/ConnexionModels";
interface EventTargetFiles extends EventTarget { interface EventTargetFiles extends EventTarget {
files: Array<File>; files: Array<File>;
@ -23,7 +23,7 @@
const AUDIO_TYPE = AdminMessageEventTypes.audio; const AUDIO_TYPE = AdminMessageEventTypes.audio;
export const handleSending = { export const handleSending = {
async sendAudioMessage() { async sendAudioMessage(broadcast: boolean) {
if (gameScene == undefined) { if (gameScene == undefined) {
return; return;
} }
@ -38,13 +38,13 @@
fd.append('file', selectedFile); fd.append('file', selectedFile);
const res = await gameScene.connection?.uploadAudio(fd); const res = await gameScene.connection?.uploadAudio(fd);
const GlobalMessage: PlayGlobalMessageInterface = { const audioGlobalMessage: PlayGlobalMessageInterface = {
id: (res as { id: string }).id, content: (res as { path: string }).path,
message: (res as { path: string }).path, type: AUDIO_TYPE,
type: AUDIO_TYPE broadcastToWorld: broadcast
} }
inputAudio.value = ''; inputAudio.value = '';
gameScene.connection?.emitGlobalMessage(GlobalMessage); gameScene.connection?.emitGlobalMessage(audioGlobalMessage);
disableConsole(); disableConsole();
} }
} }

View File

@ -0,0 +1,96 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {banMessageVisibleStore, banMessageContentStore} from "../../Stores/TypeMessageStore/BanMessageStore";
import {onMount} from "svelte";
const text = $banMessageContentStore;
const NAME_BUTTON = 'Ok';
let nbSeconds = 10;
let nameButton = '';
onMount(() => {
timeToRead()
})
function timeToRead() {
nbSeconds -= 1;
nameButton = nbSeconds.toString();
if ( nbSeconds > 0 ) {
setTimeout( () => {
timeToRead();
}, 1000);
} else {
nameButton = NAME_BUTTON;
}
}
function closeBanMessage() {
banMessageVisibleStore.set(false);
}
</script>
<div class="main-ban-message nes-container is-rounded" transition:fly="{{ y: -1000, duration: 500 }}">
<h2 class="title-ban-message"><img src="resources/logos/report.svg" alt="***"/> Important message <img src="resources/logos/report.svg" alt="***"/></h2>
<div class="content-ban-message">
<p>{text}</p>
</div>
<div class="footer-ban-message">
<button type="button" class="nes-btn {nameButton === NAME_BUTTON ? 'is-primary' : 'is-error'}" disabled="{!(nameButton === NAME_BUTTON)}" on:click|preventDefault={closeBanMessage}>{nameButton}</button>
</div>
<audio id="report-message" autoplay>
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio>
</div>
<style lang="scss">
div.main-ban-message {
display: flex;
flex-direction: column;
position: relative;
top: 15vh;
height: 70vh;
width: 60vw;
margin-left: auto;
margin-right: auto;
padding-bottom: 0;
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
h2.title-ban-message {
flex: 1 1 auto;
max-height: 50px;
margin-bottom: 20px;
text-align: center;
img {
height: 50px;
}
}
div.content-ban-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
overflow: auto;
p {
white-space: pre-wrap;
}
}
div.footer-ban-message {
height: 50px;
margin-top: 10px;
text-align: center;
button {
width: 88px;
height: 44px;
}
}
}
</style>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {textMessageContentStore, textMessageVisibleStore} from "../../Stores/TypeMessageStore/TextMessageStore";
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
const content = JSON.parse($textMessageContentStore);
const converter = new QuillDeltaToHtmlConverter(content.ops, {inlineStyles: true});
const NAME_BUTTON = 'Ok';
function closeTextMessage() {
textMessageVisibleStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeTextMessage();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<div class="main-text-message nes-container is-rounded" transition:fly="{{ x: -1000, duration: 500 }}">
<div class="content-text-message">
{@html converter.convert()}
</div>
<div class="footer-text-message">
<button type="button" class="nes-btn is-primary" on:click|preventDefault={closeTextMessage}>{NAME_BUTTON}</button>
</div>
</div>
<style lang="scss">
div.main-text-message {
display: flex;
flex-direction: column;
max-height: 25vh;
width: 80vw;
margin-right: auto;
margin-left: auto;
padding-bottom: 0;
pointer-events: auto;
background-color: #333333;
div.content-text-message {
flex: 1 1 auto;
max-height: calc(100% - 50px);
color: whitesmoke;
overflow: auto;
}
div.footer-text-message {
height: 50px;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,37 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {userIsAdminStore} from "../../Stores/GameStore";
import {ADMIN_URL} from "../../Enum/EnvironmentVariable";
const upgradeLink = ADMIN_URL+'/pricing';
</script>
<main class="warningMain" transition:fly="{{ y: -200, duration: 500 }}">
<h2>Warning!</h2>
{#if $userIsAdminStore}
<p>This world is close to its limit!. You can upgrade its capacity <a href="{upgradeLink}" target="_blank">here</a></p>
{:else}
<p>This world is close to its limit!</p>
{/if}
</main>
<style lang="scss">
main.warningMain {
pointer-events: auto;
width: 100vw;
background-color: red;
text-align: center;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
font-family: Lato;
min-width: 300px;
opacity: 0.9;
z-index: 2;
h2 {
padding: 5px;
}
}
</style>

View File

@ -14,6 +14,7 @@ class ConnectionManager {
private connexionType?: GameConnexionTypes; private connexionType?: GameConnexionTypes;
private reconnectingTimeout: NodeJS.Timeout | null = null; private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false; private _unloading: boolean = false;
private authToken: string | null = null;
private serviceWorker?: _ServiceWorker; private serviceWorker?: _ServiceWorker;
@ -27,21 +28,54 @@ class ConnectionManager {
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout); 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 * Tries to login to the node server and return the starting map url to be loaded
*/ */
public async initGameConnexion(): Promise<Room> { public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType(); const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType; this.connexionType = connexionType;
let room: Room | null = null; let room: Room | null = null;
if (connexionType === GameConnexionTypes.register) { 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 organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then( const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
(res) => res.data (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.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
const roomUrl = data.roomUrl; const roomUrl = data.roomUrl;
@ -61,24 +95,12 @@ class ConnectionManager {
connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.anonymous ||
connexionType === GameConnexionTypes.empty connexionType === GameConnexionTypes.empty
) { ) {
let localUser = localUserStore.getLocalUser(); this.authToken = localUserStore.getAuthToken();
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) { //todo: add here some kind of warning if authToken has expired.
this.localUser = localUser; if (!this.authToken) {
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 {
await this.anonymousLogin(); await this.anonymousLogin();
} }
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
localUser = localUserStore.getLocalUser();
if (!localUser) {
throw "Error to store local user data";
}
let roomPath: string; let roomPath: string;
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
@ -97,19 +119,18 @@ class ConnectionManager {
room = await Room.createRoom(new URL(roomPath)); room = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) { if (room.textures != undefined && room.textures.length > 0) {
//check if texture was changed //check if texture was changed
if (localUser.textures.length === 0) { if (this.localUser.textures.length === 0) {
localUser.textures = room.textures; this.localUser.textures = room.textures;
} else { } else {
room.textures.forEach((newTexture) => { room.textures.forEach((newTexture) => {
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id); const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
if (localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1) { if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return; return;
} }
localUser?.textures.push(newTexture); this.localUser.textures.push(newTexture);
}); });
} }
this.localUser = localUser; localUserStore.saveUser(this.localUser);
localUserStore.saveUser(localUser);
} }
} }
if (room == undefined) { if (room == undefined) {
@ -120,21 +141,19 @@ class ConnectionManager {
return Promise.resolve(room); return Promise.resolve(room);
} }
private async verifyToken(token: string): Promise<void> {
await Axios.get(`${PUSHER_URL}/verify`, { params: { token } });
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> { public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data); 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) { if (!isBenchmark) {
// In benchmark, we don't have a local storage. // In benchmark, we don't have a local storage.
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
localUserStore.setAuthToken(this.authToken);
} }
} }
public initBenchmark(): void { public initBenchmark(): void {
this.localUser = new LocalUser("", "test", []); this.localUser = new LocalUser("", []);
} }
public connectToRoomSocket( public connectToRoomSocket(
@ -147,7 +166,7 @@ class ConnectionManager {
): Promise<OnConnectInterface> { ): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => { return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection( const connection = new RoomConnection(
this.localUser.jwtToken, this.authToken,
roomUrl, roomUrl,
name, name,
characterLayers, characterLayers,

View File

@ -110,9 +110,9 @@ export interface RoomJoinedMessageInterface {
} }
export interface PlayGlobalMessageInterface { export interface PlayGlobalMessageInterface {
id: string;
type: string; type: string;
message: string; content: string;
broadcastToWorld: boolean;
} }
export interface OnConnectInterface { export interface OnConnectInterface {

View File

@ -1,10 +1,10 @@
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable"; import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
export interface CharacterTexture { export interface CharacterTexture {
id: number, id: number;
level: number, level: number;
url: string, url: string;
rights: string rights: string;
} }
export const maxUserNameLength: number = MAX_USERNAME_LENGTH; export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
@ -24,6 +24,5 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
} }
export class LocalUser { 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,4 +1,5 @@
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser"; import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
import { v4 as uuidv4 } from "uuid";
const playerNameKey = "playerName"; const playerNameKey = "playerName";
const selectedPlayerKey = "selectedPlayer"; const selectedPlayerKey = "selectedPlayer";
@ -12,6 +13,9 @@ const audioPlayerMuteKey = "audioMute";
const helpCameraSettingsShown = "helpCameraSettingsShown"; const helpCameraSettingsShown = "helpCameraSettingsShown";
const fullscreenKey = "fullscreen"; const fullscreenKey = "fullscreen";
const lastRoomUrl = "lastRoomUrl"; const lastRoomUrl = "lastRoomUrl";
const authToken = "authToken";
const state = "state";
const nonce = "nonce";
class LocalUserStore { class LocalUserStore {
saveUser(localUser: LocalUser) { saveUser(localUser: LocalUser) {
@ -116,6 +120,36 @@ class LocalUserStore {
getLastRoomUrl(): string { getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? ""; 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;
}
} }
export const localUserStore = new LocalUserStore(); export const localUserStore = new LocalUserStore();

View File

@ -32,7 +32,8 @@ import {
EmotePromptMessage, EmotePromptMessage,
SendUserMessage, SendUserMessage,
BanUserMessage, BanUserMessage,
VariableMessage, ErrorMessage, VariableMessage,
ErrorMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer"; import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
@ -54,9 +55,9 @@ import {
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import { adminMessagesService } from "./AdminMessagesService"; import { adminMessagesService } from "./AdminMessagesService";
import { worldFullMessageStream } from "./WorldFullMessageStream"; import { worldFullMessageStream } from "./WorldFullMessageStream";
import { worldFullWarningStream } from "./WorldFullWarningStream";
import { connectionManager } from "./ConnectionManager"; import { connectionManager } from "./ConnectionManager";
import { emoteEventStream } from "./EmoteEventStream"; import { emoteEventStream } from "./EmoteEventStream";
import { warningContainerStore } from "../Stores/MenuStore";
const manualPingDelay = 20000; 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]" * @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( public constructor(
@ -167,7 +168,7 @@ export class RoomConnection implements RoomConnection {
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote()); emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else if (subMessage.hasErrormessage()) { } else if (subMessage.hasErrormessage()) {
const errorMessage = subMessage.getErrormessage() as ErrorMessage; 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()) { } else if (subMessage.hasVariablemessage()) {
event = EventMessage.SET_VARIABLE; event = EventMessage.SET_VARIABLE;
payload = subMessage.getVariablemessage(); payload = subMessage.getVariablemessage();
@ -192,7 +193,14 @@ export class RoomConnection implements RoomConnection {
try { try {
variables.set(variable.getName(), JSON.parse(variable.getValue())); variables.set(variable.getName(), JSON.parse(variable.getValue()));
} catch (e) { } 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()) { } else if (message.hasWorldfullmessage()) {
worldFullMessageStream.onMessage(); worldFullMessageStream.onMessage();
this.closed = true; 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()) { } else if (message.hasWorldconnexionmessage()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage()); worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
this.closed = true; this.closed = true;
@ -236,7 +247,7 @@ export class RoomConnection implements RoomConnection {
} else if (message.hasBanusermessage()) { } else if (message.hasBanusermessage()) {
adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage); adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
} else if (message.hasWorldfullwarningmessage()) { } else if (message.hasWorldfullwarningmessage()) {
worldFullWarningStream.onMessage(); warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) { } else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed. //todo: implement a way to notify the user the room was refreshed.
} else { } else {
@ -586,7 +597,7 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) { /* public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => { return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
callback({ callback({
id: message.getId(), id: message.getId(),
@ -594,7 +605,7 @@ export class RoomConnection implements RoomConnection {
message: message.getMessage(), message: message.getMessage(),
}); });
}); });
} }*/
public receiveStopGlobalMessage(callback: (messageId: string) => void) { public receiveStopGlobalMessage(callback: (messageId: string) => void) {
return this.onMessage(EventMessage.STOP_GLOBAL_MESSAGE, (message: StopGlobalMessage) => { return this.onMessage(EventMessage.STOP_GLOBAL_MESSAGE, (message: StopGlobalMessage) => {
@ -608,11 +619,11 @@ export class RoomConnection implements RoomConnection {
}); });
} }
public emitGlobalMessage(message: PlayGlobalMessageInterface) { public emitGlobalMessage(message: PlayGlobalMessageInterface): void {
const playGlobalMessage = new PlayGlobalMessage(); const playGlobalMessage = new PlayGlobalMessage();
playGlobalMessage.setId(message.id);
playGlobalMessage.setType(message.type); playGlobalMessage.setType(message.type);
playGlobalMessage.setMessage(message.message); playGlobalMessage.setContent(message.content);
playGlobalMessage.setBroadcasttoworld(message.broadcastToWorld);
const clientToServerMessage = new ClientToServerMessage(); const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setPlayglobalmessage(playGlobalMessage); clientToServerMessage.setPlayglobalmessage(playGlobalMessage);
@ -659,7 +670,14 @@ export class RoomConnection implements RoomConnection {
try { try {
value = JSON.parse(serializedValue); value = JSON.parse(serializedValue);
} catch (e) { } 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); 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();

View File

@ -1,22 +1,24 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json'; const START_ROOM_URL: string =
const PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost'; process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost'; const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || ""; const TURN_SERVER: string = process.env.TURN_SERVER || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true"; const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true"; const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true";
const TURN_USER: string = process.env.TURN_USER || ''; const TURN_USER: string = process.env.TURN_USER || "";
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || ''; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || "";
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true"; const JITSI_PRIVATE_MODE: boolean = process.env.JITSI_PRIVATE_MODE == "true";
const POSITION_DELAY = 200; // Wait 200ms between sending position events const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8; export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || "") || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4'); export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true'; export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) ); export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;
export { export {
DEBUG_MODE, DEBUG_MODE,
@ -32,5 +34,5 @@ export {
TURN_USER, TURN_USER,
TURN_PASSWORD, TURN_PASSWORD,
JITSI_URL, JITSI_URL,
JITSI_PRIVATE_MODE JITSI_PRIVATE_MODE,
} };

View File

@ -1,14 +0,0 @@
export const warningContainerKey = 'warningContainer';
export const warningContainerHtml = 'resources/html/warningContainer.html';
export class WarningContainer extends Phaser.GameObjects.DOMElement {
constructor(scene: Phaser.Scene) {
super(scene, 100, 0);
this.setOrigin(0, 0);
this.createFromCache(warningContainerKey);
this.scene.add.existing(this);
}
}

View File

@ -1,5 +1,4 @@
import type { Subscription } from "rxjs"; import type { Subscription } from "rxjs";
import { GlobalMessageManager } from "../../Administration/GlobalMessageManager";
import { userMessageManager } from "../../Administration/UserMessageManager"; import { userMessageManager } from "../../Administration/UserMessageManager";
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
@ -75,8 +74,6 @@ import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey }
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { EmoteManager } from "./EmoteManager"; import { EmoteManager } from "./EmoteManager";
import EVENT_TYPE = Phaser.Scenes.Events; import EVENT_TYPE = Phaser.Scenes.Events;
import RenderTexture = Phaser.GameObjects.RenderTexture;
import Tilemap = Phaser.Tilemaps.Tilemap;
import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent"; import type { HasPlayerMovedEvent } from "../../Api/Events/HasPlayerMovedEvent";
import AnimatedTiles from "phaser-animated-tiles"; import AnimatedTiles from "phaser-animated-tiles";
@ -88,6 +85,8 @@ import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStor
import { SharedVariablesManager } from "./SharedVariablesManager"; import { SharedVariablesManager } from "./SharedVariablesManager";
import { playersStore } from "../../Stores/PlayersStore"; import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore"; import { chatVisibilityStore } from "../../Stores/ChatStore";
import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -155,7 +154,6 @@ export class GameScene extends DirtyScene {
private playersPositionInterpolator = new PlayersPositionInterpolator(); private playersPositionInterpolator = new PlayersPositionInterpolator();
public connection: RoomConnection | undefined; public connection: RoomConnection | undefined;
private simplePeer!: SimplePeer; private simplePeer!: SimplePeer;
private GlobalMessageManager!: GlobalMessageManager;
private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>; private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>;
private connectionAnswerPromiseResolve!: ( private connectionAnswerPromiseResolve!: (
value: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface> value: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>
@ -221,6 +219,9 @@ export class GameScene extends DirtyScene {
//hook preload scene //hook preload scene
preload(): void { preload(): void {
//initialize frame event of scripting API
this.listenToIframeEvents();
const localUser = localUserStore.getLocalUser(); const localUser = localUserStore.getLocalUser();
const textures = localUser?.textures; const textures = localUser?.textures;
if (textures) { if (textures) {
@ -549,7 +550,6 @@ export class GameScene extends DirtyScene {
); );
this.triggerOnMapLayerPropertyChange(); this.triggerOnMapLayerPropertyChange();
this.listenToIframeEvents();
if (!this.room.isDisconnected()) { if (!this.room.isDisconnected()) {
this.connect(); this.connect();
@ -604,6 +604,8 @@ export class GameScene extends DirtyScene {
playersStore.connectToRoomConnection(this.connection); playersStore.connectToRoomConnection(this.connection);
userIsAdminStore.set(this.connection.hasTag("admin"));
this.connection.onUserJoins((message: MessageUserJoined) => { this.connection.onUserJoins((message: MessageUserJoined) => {
const userMessage: AddPlayerInterface = { const userMessage: AddPlayerInterface = {
userId: message.userId, userId: message.userId,
@ -690,7 +692,6 @@ export class GameScene extends DirtyScene {
peerStore.connectToSimplePeer(this.simplePeer); peerStore.connectToSimplePeer(this.simplePeer);
screenSharingPeerStore.connectToSimplePeer(this.simplePeer); screenSharingPeerStore.connectToSimplePeer(this.simplePeer);
videoFocusStore.connectToSimplePeer(this.simplePeer); videoFocusStore.connectToSimplePeer(this.simplePeer);
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
const self = this; const self = this;
@ -1087,6 +1088,7 @@ export class GameScene extends DirtyScene {
for (const eventTile of eventTiles) { for (const eventTile of eventTiles) {
this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer); this.gameMap.putTile(eventTile.tile, eventTile.x, eventTile.y, eventTile.layer);
} }
this.markDirty();
}) })
); );
@ -1116,6 +1118,71 @@ export class GameScene extends DirtyScene {
} }
})) }))
iframeListener.registerAnswerer("loadTileset", (eventTileset) => {
return this.connectionAnswerPromise.then(() => {
const jsonTilesetDir = eventTileset.url.substr(0, eventTileset.url.lastIndexOf("/"));
//Initialise the firstgid to 1 because if there is no tileset in the tilemap, the firstgid will be 1
let newFirstgid = 1;
const lastTileset = this.mapFile.tilesets[this.mapFile.tilesets.length - 1];
if (lastTileset) {
//If there is at least one tileset in the tilemap then calculate the firstgid of the new tileset
newFirstgid = lastTileset.firstgid + lastTileset.tilecount;
}
return new Promise((resolve, reject) => {
this.load.on("filecomplete-json-" + eventTileset.url, () => {
let jsonTileset = this.cache.json.get(eventTileset.url);
const imageUrl = jsonTilesetDir + "/" + jsonTileset.image;
this.load.image(imageUrl, imageUrl);
this.load.on("filecomplete-image-" + imageUrl, () => {
//Add the firstgid of the tileset to the json file
jsonTileset = { ...jsonTileset, firstgid: newFirstgid };
this.mapFile.tilesets.push(jsonTileset);
this.Map.tilesets.push(
new Tileset(
jsonTileset.name,
jsonTileset.firstgid,
jsonTileset.tileWidth,
jsonTileset.tileHeight,
jsonTileset.margin,
jsonTileset.spacing,
jsonTileset.tiles
)
);
this.Terrains.push(
this.Map.addTilesetImage(
jsonTileset.name,
imageUrl,
jsonTileset.tilewidth,
jsonTileset.tileheight,
jsonTileset.margin,
jsonTileset.spacing
)
);
//destroy the tilemapayer because they are unique and we need to reuse their key and layerdData
for (const layer of this.Map.layers) {
layer.tilemapLayer.destroy(false);
}
//Create a new GameMap with the changed file
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
//Destroy the colliders of the old tilemapLayer
this.physics.add.world.colliders.destroy();
//Create new colliders with the new GameMap
this.createCollisionWithPlayer();
//Create new trigger with the new GameMap
this.triggerOnMapLayerPropertyChange();
resolve(newFirstgid);
});
});
this.load.on("loaderror", () => {
console.error("Error while loading " + eventTileset.url + ".");
reject(-1);
});
this.load.json(eventTileset.url, eventTileset.url);
this.load.start();
});
});
});
} }
private setPropertyLayer( private setPropertyLayer(
@ -1183,7 +1250,7 @@ export class GameScene extends DirtyScene {
let targetRoom: Room; let targetRoom: Room;
try { try {
targetRoom = await Room.createRoom(roomUrl); targetRoom = await Room.createRoom(roomUrl);
} catch (e: unknown) { } catch (e) {
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e); console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
this.mapTransitioning = false; this.mapTransitioning = false;
return; return;
@ -1237,6 +1304,7 @@ export class GameScene extends DirtyScene {
this.chatVisibilityUnsubscribe(); this.chatVisibilityUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer("getState"); iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("loadTileset");
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();
@ -1309,7 +1377,7 @@ export class GameScene extends DirtyScene {
try { try {
const room = await Room.createRoom(exitRoomPath); const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene); return gameManager.loadMap(room, this.scene);
} catch (e: unknown) { } catch (e) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e); console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
} }
} }

View File

@ -6,8 +6,6 @@ import { localUserStore } from "../../Connexion/LocalUserStore";
import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu"; import { gameReportKey, gameReportRessource, ReportMenu } from "./ReportMenu";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../../Url/UrlManager"; import { GameConnexionTypes } from "../../Url/UrlManager";
import { WarningContainer, warningContainerHtml, warningContainerKey } from "../Components/WarningContainer";
import { worldFullWarningStream } from "../../Connexion/WorldFullWarningStream";
import { menuIconVisible } from "../../Stores/MenuStore"; import { menuIconVisible } from "../../Stores/MenuStore";
import { videoConstraintStore } from "../../Stores/MediaStore"; import { videoConstraintStore } from "../../Stores/MediaStore";
import { showReportScreenStore } from "../../Stores/ShowReportScreenStore"; import { showReportScreenStore } from "../../Stores/ShowReportScreenStore";
@ -21,6 +19,7 @@ import { get } from "svelte/store";
import { playersStore } from "../../Stores/PlayersStore"; import { playersStore } from "../../Stores/PlayersStore";
import { mediaManager } from "../../WebRtc/MediaManager"; import { mediaManager } from "../../WebRtc/MediaManager";
import { chatVisibilityStore } from "../../Stores/ChatStore"; import { chatVisibilityStore } from "../../Stores/ChatStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
export const MenuSceneName = "MenuScene"; export const MenuSceneName = "MenuScene";
const gameMenuKey = "gameMenu"; const gameMenuKey = "gameMenu";
@ -45,8 +44,6 @@ export class MenuScene extends Phaser.Scene {
private gameQualityValue: number; private gameQualityValue: number;
private videoQualityValue: number; private videoQualityValue: number;
private menuButton!: Phaser.GameObjects.DOMElement; private menuButton!: Phaser.GameObjects.DOMElement;
private warningContainer: WarningContainer | null = null;
private warningContainerTimeout: NodeJS.Timeout | null = null;
private subscriptions = new Subscription(); private subscriptions = new Subscription();
constructor() { constructor() {
super({ key: MenuSceneName }); super({ key: MenuSceneName });
@ -91,7 +88,6 @@ export class MenuScene extends Phaser.Scene {
this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html"); this.load.html(gameSettingsMenuKey, "resources/html/gameQualityMenu.html");
this.load.html(gameShare, "resources/html/gameShare.html"); this.load.html(gameShare, "resources/html/gameShare.html");
this.load.html(gameReportKey, gameReportRessource); this.load.html(gameReportKey, gameReportRessource);
this.load.html(warningContainerKey, warningContainerHtml);
} }
create() { create() {
@ -147,7 +143,6 @@ export class MenuScene extends Phaser.Scene {
this.menuElement.addListener("click"); this.menuElement.addListener("click");
this.menuElement.on("click", this.onMenuClick.bind(this)); this.menuElement.on("click", this.onMenuClick.bind(this));
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
chatVisibilityStore.subscribe((v) => { chatVisibilityStore.subscribe((v) => {
this.menuButton.setVisible(!v); this.menuButton.setVisible(!v);
}); });
@ -194,20 +189,6 @@ export class MenuScene extends Phaser.Scene {
}); });
} }
private showWorldCapacityWarning() {
if (!this.warningContainer) {
this.warningContainer = new WarningContainer(this);
}
if (this.warningContainerTimeout) {
clearTimeout(this.warningContainerTimeout);
}
this.warningContainerTimeout = setTimeout(() => {
this.warningContainer?.destroy();
this.warningContainer = null;
this.warningContainerTimeout = null;
}, 120000);
}
private closeSideMenu(): void { private closeSideMenu(): void {
if (!this.sideMenuOpened) return; if (!this.sideMenuOpened) return;
this.sideMenuOpened = false; this.sideMenuOpened = false;
@ -363,6 +344,9 @@ export class MenuScene extends Phaser.Scene {
case "editGameSettingsButton": case "editGameSettingsButton":
this.openGameSettingsMenu(); this.openGameSettingsMenu();
break; break;
case "oidcLogin":
connectionManager.loadOpenIDScreen();
break;
case "toggleFullscreen": case "toggleFullscreen":
this.toggleFullscreen(); this.toggleFullscreen();
break; break;
@ -403,6 +387,10 @@ export class MenuScene extends Phaser.Scene {
private gotToCreateMapPage() { private gotToCreateMapPage() {
//const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html'; //const sparkHost = 'https://'+window.location.host.replace('play.', '')+'/choose-map.html';
//TODO fix me: this button can to send us on WorkAdventure BO. //TODO fix me: this button can to send us on WorkAdventure BO.
//const sparkHost = ADMIN_URL + "/getting-started";
//The redirection must be only on workadventu.re domain
//To day the domain staging cannot be use by customer
const sparkHost = "https://workadventu.re/getting-started"; const sparkHost = "https://workadventu.re/getting-started";
window.open(sparkHost, "_blank"); window.open(sparkHost, "_blank");
} }

View File

@ -1,12 +1,12 @@
import type { Direction } from "../../types"; import type { Direction } from "../../types";
import type {GameScene} from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import { touchScreenManager } from "../../Touch/TouchScreenManager";
import {MobileJoystick} from "../Components/MobileJoystick"; import { MobileJoystick } from "../Components/MobileJoystick";
import {enableUserInputsStore} from "../../Stores/UserInputStore"; import { enableUserInputsStore } from "../../Stores/UserInputStore";
interface UserInputManagerDatum { interface UserInputManagerDatum {
keyInstance: Phaser.Input.Keyboard.Key; keyInstance: Phaser.Input.Keyboard.Key;
event: UserInputEvent event: UserInputEvent;
} }
export enum UserInputEvent { export enum UserInputEvent {
@ -20,10 +20,9 @@ export enum UserInputEvent {
JoystickMove, JoystickMove,
} }
//we cannot use a map structure so we have to create a replacement //we cannot use a map structure so we have to create a replacement
export class ActiveEventList { export class ActiveEventList {
private eventMap : Map<UserInputEvent, boolean> = new Map<UserInputEvent, boolean>(); private eventMap: Map<UserInputEvent, boolean> = new Map<UserInputEvent, boolean>();
get(event: UserInputEvent): boolean { get(event: UserInputEvent): boolean {
return this.eventMap.get(event) || false; return this.eventMap.get(event) || false;
@ -43,7 +42,7 @@ export class ActiveEventList {
export class UserInputManager { export class UserInputManager {
private KeysCode!: UserInputManagerDatum[]; private KeysCode!: UserInputManagerDatum[];
private Scene: GameScene; private Scene: GameScene;
private isInputDisabled : boolean; private isInputDisabled: boolean;
private joystick!: MobileJoystick; private joystick!: MobileJoystick;
private joystickEvents = new ActiveEventList(); private joystickEvents = new ActiveEventList();
@ -61,8 +60,8 @@ export class UserInputManager {
} }
enableUserInputsStore.subscribe((enable) => { enableUserInputsStore.subscribe((enable) => {
enable ? this.restoreControls() : this.disableControls() enable ? this.restoreControls() : this.disableControls();
}) });
} }
initVirtualJoystick() { initVirtualJoystick() {
@ -91,39 +90,81 @@ export class UserInputManager {
}); });
} }
initKeyBoardEvent(){ initKeyBoardEvent() {
this.KeysCode = [ this.KeysCode = [
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) }, {
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false) }, event: UserInputEvent.MoveUp,
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) }, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false),
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false) }, },
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) }, {
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) }, event: UserInputEvent.MoveUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false),
},
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false),
},
{
event: UserInputEvent.MoveDown,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false),
},
{
event: UserInputEvent.MoveRight,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false),
},
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false) }, {
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false) }, event: UserInputEvent.MoveUp,
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false) }, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP, false),
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false) }, },
{
event: UserInputEvent.MoveLeft,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT, false),
},
{
event: UserInputEvent.MoveDown,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN, false),
},
{
event: UserInputEvent.MoveRight,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT, false),
},
{event: UserInputEvent.SpeedUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false) }, {
event: UserInputEvent.SpeedUp,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT, false),
},
{event: UserInputEvent.Interact, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false) }, {
{event: UserInputEvent.Interact, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false) }, event: UserInputEvent.Interact,
{event: UserInputEvent.Shout, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false) }, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E, false),
},
{
event: UserInputEvent.Interact,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE, false),
},
{
event: UserInputEvent.Shout,
keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F, false),
},
]; ];
} }
clearAllListeners(){ clearAllListeners() {
this.Scene.input.keyboard.removeAllListeners(); this.Scene.input.keyboard.removeAllListeners();
} }
//todo: should we also disable the joystick? //todo: should we also disable the joystick?
disableControls(){ disableControls() {
this.Scene.input.keyboard.removeAllKeys(); this.Scene.input.keyboard.removeAllKeys();
this.isInputDisabled = true; this.isInputDisabled = true;
} }
restoreControls(){ restoreControls() {
this.initKeyBoardEvent(); this.initKeyBoardEvent();
this.isInputDisabled = false; this.isInputDisabled = false;
} }
@ -135,27 +176,27 @@ export class UserInputManager {
this.joystickEvents.forEach((value, key) => { this.joystickEvents.forEach((value, key) => {
if (value) { if (value) {
switch (key) { switch (key) {
case UserInputEvent.MoveUp: case UserInputEvent.MoveUp:
case UserInputEvent.MoveDown: case UserInputEvent.MoveDown:
this.joystickForceAccuY += this.joystick.forceY; this.joystickForceAccuY += this.joystick.forceY;
if (Math.abs(this.joystickForceAccuY) > this.joystickForceThreshold) { if (Math.abs(this.joystickForceAccuY) > this.joystickForceThreshold) {
eventsMap.set(key, value); eventsMap.set(key, value);
this.joystickForceAccuY = 0; this.joystickForceAccuY = 0;
} }
break; break;
case UserInputEvent.MoveLeft: case UserInputEvent.MoveLeft:
case UserInputEvent.MoveRight: case UserInputEvent.MoveRight:
this.joystickForceAccuX += this.joystick.forceX; this.joystickForceAccuX += this.joystick.forceX;
if (Math.abs(this.joystickForceAccuX) > this.joystickForceThreshold) { if (Math.abs(this.joystickForceAccuX) > this.joystickForceThreshold) {
eventsMap.set(key, value); eventsMap.set(key, value);
this.joystickForceAccuX = 0; this.joystickForceAccuX = 0;
} }
break; break;
} }
} }
}); });
eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any()); eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any());
this.KeysCode.forEach(d => { this.KeysCode.forEach((d) => {
if (d.keyInstance.isDown) { if (d.keyInstance.isDown) {
eventsMap.set(d.event, true); eventsMap.set(d.event, true);
} }
@ -163,18 +204,18 @@ export class UserInputManager {
return eventsMap; return eventsMap;
} }
spaceEvent(callback : Function){ spaceEvent(callback: Function) {
this.Scene.input.keyboard.on('keyup-SPACE', (event: Event) => { this.Scene.input.keyboard.on("keyup-SPACE", (event: Event) => {
callback(); callback();
return event; return event;
}); });
} }
addSpaceEventListner(callback : Function){ addSpaceEventListner(callback: Function) {
this.Scene.input.keyboard.addListener('keyup-SPACE', callback); this.Scene.input.keyboard.addListener("keyup-SPACE", callback);
} }
removeSpaceEventListner(callback : Function){ removeSpaceEventListner(callback: Function) {
this.Scene.input.keyboard.removeListener('keyup-SPACE', callback); this.Scene.input.keyboard.removeListener("keyup-SPACE", callback);
} }
destroy(): void { destroy(): void {
@ -182,8 +223,11 @@ export class UserInputManager {
} }
private initMouseWheel() { private initMouseWheel() {
this.Scene.input.on('wheel', (pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => { this.Scene.input.on(
this.Scene.zoomByFactor(1 - deltaY / 53 * 0.1); "wheel",
}); (pointer: unknown, gameObjects: unknown, deltaX: number, deltaY: number, deltaZ: number) => {
this.Scene.zoomByFactor(1 - (deltaY / 53) * 0.1);
}
);
} }
} }

View File

@ -2,4 +2,6 @@ import { writable } from "svelte/store";
export const userMovingStore = writable(false); export const userMovingStore = writable(false);
export const requestVisitCardsStore = writable<string|null>(null); export const requestVisitCardsStore = writable<string | null>(null);
export const userIsAdminStore = writable(false);

View File

@ -1,3 +1,23 @@
import { derived, writable, Writable } from "svelte/store"; import { writable } from "svelte/store";
import Timeout = NodeJS.Timeout;
export const menuIconVisible = writable(false); export const menuIconVisible = writable(false);
let warningContainerTimeout: Timeout | null = null;
function createWarningContainerStore() {
const { subscribe, set } = writable<boolean>(false);
return {
subscribe,
activateWarningContainer() {
set(true);
if (warningContainerTimeout) clearTimeout(warningContainerTimeout);
warningContainerTimeout = setTimeout(() => {
set(false);
warningContainerTimeout = null;
}, 120000);
},
};
}
export const warningContainerStore = createWarningContainerStore();

View File

@ -0,0 +1,5 @@
import { writable } from "svelte/store";
export const banMessageVisibleStore = writable(false);
export const banMessageContentStore = writable("");

View File

@ -0,0 +1,5 @@
import { writable } from "svelte/store";
export const textMessageVisibleStore = writable(false);
export const textMessageContentStore = writable("");

View File

@ -1,45 +1,46 @@
import type {Room} from "../Connexion/Room"; import type { Room } from "../Connexion/Room";
export enum GameConnexionTypes { export enum GameConnexionTypes {
anonymous=1, anonymous = 1,
organization, organization,
register, register,
empty, empty,
unknown, unknown,
jwt,
} }
//this class is responsible with analysing and editing the game's url //this class is responsible with analysing and editing the game's url
class UrlManager { class UrlManager {
//todo: use that to detect if we can find a token in localstorage
public getGameConnexionType(): GameConnexionTypes { public getGameConnexionType(): GameConnexionTypes {
const url = window.location.pathname.toString(); const url = window.location.pathname.toString();
if (url.includes('_/')) { if (url === "/jwt") {
return GameConnexionTypes.jwt;
} else if (url.includes("_/")) {
return GameConnexionTypes.anonymous; return GameConnexionTypes.anonymous;
} else if (url.includes('@/')) { } else if (url.includes("@/")) {
return GameConnexionTypes.organization; return GameConnexionTypes.organization;
} else if(url.includes('register/')) { } else if (url.includes("register/")) {
return GameConnexionTypes.register; return GameConnexionTypes.register;
} else if(url === '/') { } else if (url === "/") {
return GameConnexionTypes.empty; return GameConnexionTypes.empty;
} else { } else {
return GameConnexionTypes.unknown; return GameConnexionTypes.unknown;
} }
} }
public getOrganizationToken(): string|null { public getOrganizationToken(): string | null {
const match = /\/register\/(.+)/.exec(window.location.pathname.toString()); const match = /\/register\/(.+)/.exec(window.location.pathname.toString());
return match ? match [1] : null; return match ? match[1] : null;
} }
public pushRoomIdToUrl(room:Room): void { public pushRoomIdToUrl(room: Room): void {
if (window.location.pathname === room.id) return; if (window.location.pathname === room.id) return;
const hash = window.location.hash; const hash = window.location.hash;
const search = room.search.toString(); const search = room.search.toString();
history.pushState({}, 'WorkAdventure', room.id+(search?'?'+search:'')+hash); history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);
} }
public getStartLayerNameFromUrl(): string|null { public getStartLayerNameFromUrl(): string | null {
const hash = window.location.hash; const hash = window.location.hash;
return hash.length > 1 ? hash.substring(1) : null; return hash.length > 1 ? hash.substring(1) : null;
} }

View File

@ -9,8 +9,7 @@ section.section-input-send-text {
} }
div.input-send-text{ div.input-send-text{
height: auto; height: calc(100% - 100px);
max-height: calc(100% - 100px);
overflow: auto; overflow: auto;
color: whitesmoke; color: whitesmoke;
@ -20,5 +19,13 @@ section.section-input-send-text {
color: whitesmoke; color: whitesmoke;
font-size: 1rem; font-size: 1rem;
} }
.ql-tooltip {
top: 40% !important;
left: 20% !important;
color: whitesmoke;
background-color: #333333;
}
} }
} }

View File

@ -1,28 +1,28 @@
import "jasmine"; import "jasmine";
import {getRessourceDescriptor} from "../../../src/Phaser/Entity/PlayerTexturesLoadingManager"; import { getRessourceDescriptor } from "../../../src/Phaser/Entity/PlayerTexturesLoadingManager";
describe("getRessourceDescriptor()", () => { describe("getRessourceDescriptor()", () => {
it(", if given a valid descriptor as parameter, should return it", () => { it(", if given a valid descriptor as parameter, should return it", () => {
const desc = getRessourceDescriptor({name: 'name', img: 'url'}); const desc = getRessourceDescriptor({ name: "name", img: "url" });
expect(desc.name).toEqual('name'); expect(desc.name).toEqual("name");
expect(desc.img).toEqual('url'); expect(desc.img).toEqual("url");
}); });
it(", if given a string as parameter, should search through hardcoded values", () => { it(", if given a string as parameter, should search through hardcoded values", () => {
const desc = getRessourceDescriptor('male1'); const desc = getRessourceDescriptor("male1");
expect(desc.name).toEqual('male1'); expect(desc.name).toEqual("male1");
expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png"); expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png");
}); });
it(", if given a string as parameter, should search through hardcoded values (bis)", () => { it(", if given a string as parameter, should search through hardcoded values (bis)", () => {
const desc = getRessourceDescriptor('color_2'); const desc = getRessourceDescriptor("color_2");
expect(desc.name).toEqual('color_2'); expect(desc.name).toEqual("color_2");
expect(desc.img).toEqual("resources/customisation/character_color/character_color1.png"); expect(desc.img).toEqual("resources/customisation/character_color/character_color1.png");
}); });
it(", if given a descriptor without url as parameter, should search through hardcoded values", () => { it(", if given a descriptor without url as parameter, should search through hardcoded values", () => {
const desc = getRessourceDescriptor({name: 'male1', img: ''}); const desc = getRessourceDescriptor({ name: "male1", img: "" });
expect(desc.name).toEqual('male1'); expect(desc.name).toEqual("male1");
expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png"); expect(desc.img).toEqual("resources/characters/pipoya/Male 01-1.png");
}); });
}); });

View File

@ -189,7 +189,7 @@ module.exports = {
DISABLE_NOTIFICATIONS: false, DISABLE_NOTIFICATIONS: false,
PUSHER_URL: undefined, PUSHER_URL: undefined,
UPLOADER_URL: null, UPLOADER_URL: null,
ADMIN_URL: null, ADMIN_URL: undefined,
DEBUG_MODE: null, DEBUG_MODE: null,
STUN_SERVER: null, STUN_SERVER: null,
TURN_SERVER: null, TURN_SERVER: null,

View File

@ -343,6 +343,18 @@
dependencies: dependencies:
source-map "^0.6.1" source-map "^0.6.1"
"@types/uuid@8.3.0":
version "8.3.0"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
"@types/uuidv4@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/uuidv4/-/uuidv4-5.0.0.tgz#2c94e67b0c06d5adb28fb7ced1a1b5f0866ecd50"
integrity sha512-xUrhYSJnkTq9CP79cU3svoKTLPCIbMMnu9Twf/tMpHATYSHCAAeDNeb2a/29YORhk5p4atHhCTMsIBU/tvdh6A==
dependencies:
uuidv4 "*"
"@types/webpack-dev-server@^3.11.4": "@types/webpack-dev-server@^3.11.4":
version "3.11.4" version "3.11.4"
resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07" resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07"
@ -3606,6 +3618,11 @@ lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.truncate@^4.4.2: lodash.truncate@^4.4.2:
version "4.4.2" version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
@ -4647,6 +4664,13 @@ queue-typescript@^1.0.1:
dependencies: dependencies:
linked-list-typescript "^1.0.11" linked-list-typescript "^1.0.11"
quill-delta-to-html@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/quill-delta-to-html/-/quill-delta-to-html-0.12.0.tgz#ee572cfa208390d99d7646795358ddb81eaea17c"
integrity sha512-Yy6U2e7ov+ZlrFbj5/GbqOBCRjyNu+vuphy0Pk+7668zIMTVHJglZY2JNa++1/zkSiqptPBmP/CpsDzC4Wznsw==
dependencies:
lodash.isequal "^4.5.0"
quill-delta@^3.6.2: quill-delta@^3.6.2:
version "3.6.3" version "3.6.3"
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032" resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
@ -5990,11 +6014,24 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^3.3.2, uuid@^3.4.0: uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuidv4@*, uuidv4@^6.2.10:
version "6.2.10"
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.10.tgz#42fc1c12b6f85ad536c2c5c1e836079d1e15003c"
integrity sha512-FMo1exd9l5UvoUPHRR6NrtJ/OJRePh0ca7IhPwBuMNuYRqjtuh8lE3WDxAUvZ4Yss5FbCOsPFjyWJf9lVTEmnw==
dependencies:
"@types/uuid" "8.3.0"
uuid "8.3.2"
v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0: v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"

View File

@ -0,0 +1,159 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[33, 34, 33, 34, 34, 34, 35, 37, 38, 39, 41, 42, 41, 42, 42, 42, 43, 45, 46, 47, 33, 34, 60, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 41, 42, 42, 42, 42, 42, 43, 45, 46, 47, 49, 50, 50, 50, 50, 50, 51, 53, 54, 55],
"height":10,
"id":2,
"name":"bottom",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[57, 58, 0, 0, 0, 0, 0, 0, 0, 0, 59, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":3,
"name":"openwebsite",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"https:\/\/fr.wikipedia.org\/wiki\/Wikip%C3%A9dia:Accueil_principal"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
}],
"nextlayerid":4,
"nextobjectid":1,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"scriptTileset.js"
}],
"renderorder":"right-down",
"tiledversion":"1.7.0",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"Dungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":36,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":37,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":38,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":44,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":45,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":46,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":52,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":53,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":54,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":"1.6",
"width":10
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,106 @@
{ "columns":11,
"image":"Yellow.jpg",
"imageheight":128,
"imagewidth":352,
"margin":0,
"name":"Yellow",
"spacing":0,
"tilecount":44,
"tiledversion":"1.7.0",
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
},
{
"name":"name",
"type":"string",
"value":"Mur"
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":11,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":12,
"properties":[
{
"name":"name",
"type":"string",
"value":"sol"
},
{
"name":"openWebsite",
"type":"string",
"value":"https:\/\/fr.wikipedia.org\/wiki\/Wikip%C3%A9dia:Accueil_principal"
}]
},
{
"id":13,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":22,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":23,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":24,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32,
"type":"tileset",
"version":"1.6"
}

View File

@ -0,0 +1,6 @@
WA.room.loadTileset("http://maps.workadventure.localhost/tests/LoadTileset/Yellow.json").then((firstgid) => {
WA.room.setTiles([
{x: 5, y: 5, tile: firstgid + 1, layer: 'bottom'},
{x: 5, y: 3, tile: 'sol', layer: 'bottom'}
]);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -123,9 +123,9 @@ message VariableWithTagMessage {
} }
message PlayGlobalMessage { message PlayGlobalMessage {
string id = 1; string type = 1;
string type = 2; string content = 2;
string message = 3; bool broadcastToWorld = 3;
} }
message StopGlobalMessage { message StopGlobalMessage {
@ -247,6 +247,8 @@ message RefreshRoomMessage{
message WorldFullMessage{ message WorldFullMessage{
} }
message TokenExpiredMessage{
}
message WorldConnexionMessage{ message WorldConnexionMessage{
string message = 2; string message = 2;
@ -278,6 +280,7 @@ message ServerToClientMessage {
RefreshRoomMessage refreshRoomMessage = 17; RefreshRoomMessage refreshRoomMessage = 17;
WorldConnexionMessage worldConnexionMessage = 18; WorldConnexionMessage worldConnexionMessage = 18;
EmoteEventMessage emoteEventMessage = 19; EmoteEventMessage emoteEventMessage = 19;
TokenExpiredMessage tokenExpiredMessage = 20;
} }
} }
@ -442,6 +445,7 @@ message AdminMessage {
message AdminRoomMessage { message AdminRoomMessage {
string message = 1; string message = 1;
string roomId = 2; string roomId = 2;
string type = 3;
} }
// A message sent by an administrator to absolutely everybody // A message sent by an administrator to absolutely everybody

View File

@ -49,6 +49,7 @@
"grpc": "^1.24.4", "grpc": "^1.24.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"openid-client": "^4.7.4",
"prom-client": "^12.0.0", "prom-client": "^12.0.0",
"query-string": "^6.13.3", "query-string": "^6.13.3",
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",

View File

@ -4,6 +4,7 @@ import { BaseController } from "./BaseController";
import { adminApi } from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
import { jwtTokenManager } from "../Services/JWTTokenManager"; import { jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string"; import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
export interface TokenInterface { export interface TokenInterface {
userUuid: string; userUuid: string;
@ -12,11 +13,58 @@ export interface TokenInterface {
export class AuthenticateController extends BaseController { export class AuthenticateController extends BaseController {
constructor(private App: TemplatedApp) { constructor(private App: TemplatedApp) {
super(); super();
this.openIDLogin();
this.openIDCallback();
this.register(); this.register();
this.verify();
this.anonymLogin(); this.anonymLogin();
} }
openIDLogin() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/login-screen", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { nonce, state } = parse(req.getQuery());
if (!state || !nonce) {
res.writeStatus("400 Unauthorized").end("missing state and nonce URL parameters");
return;
}
try {
const loginUri = await openIDClient.authorizationUrl(state as string, nonce as string);
res.writeStatus("302");
res.writeHeader("Location", loginUri);
return res.end();
} catch (e) {
return this.errorToResponse(e, res);
}
});
}
openIDCallback() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/login-callback", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { code, nonce } = parse(req.getQuery());
try {
const userInfo = await openIDClient.getUserInfo(code as string, nonce as string);
const email = userInfo.email || userInfo.sub;
if (!email) {
throw new Error("No email in the response");
}
const authToken = jwtTokenManager.createAuthToken(email);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken }));
} catch (e) {
return this.errorToResponse(e, res);
}
});
}
//Try to login with an admin token //Try to login with an admin token
private register() { private register() {
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
@ -39,11 +87,12 @@ export class AuthenticateController extends BaseController {
if (typeof organizationMemberToken != "string") throw new Error("No organization token"); if (typeof organizationMemberToken != "string") throw new Error("No organization token");
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
const userUuid = data.userUuid; const userUuid = data.userUuid;
const email = data.email;
const roomUrl = data.roomUrl; const roomUrl = data.roomUrl;
const mapUrlStart = data.mapUrlStart; const mapUrlStart = data.mapUrlStart;
const textures = data.textures; const textures = data.textures;
const authToken = jwtTokenManager.createJWTToken(userUuid); const authToken = jwtTokenManager.createAuthToken(email || userUuid);
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end( res.end(
@ -63,45 +112,6 @@ export class AuthenticateController extends BaseController {
}); });
} }
private verify() {
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => {
(async () => {
const query = parse(req.getQuery());
res.onAborted(() => {
console.warn("verify request was aborted");
});
try {
await jwtTokenManager.getUserUuidFromToken(query.token as string);
} catch (e) {
res.writeStatus("400 Bad Request");
this.addCorsHeaders(res);
res.end(
JSON.stringify({
success: false,
message: "Invalid JWT token",
})
);
return;
}
res.writeStatus("200 OK");
this.addCorsHeaders(res);
res.end(
JSON.stringify({
success: true,
})
);
})();
});
}
//permit to login on application. Return token to connect on Websocket IO. //permit to login on application. Return token to connect on Websocket IO.
private anonymLogin() { private anonymLogin() {
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
@ -115,7 +125,7 @@ export class AuthenticateController extends BaseController {
}); });
const userUuid = v4(); const userUuid = v4();
const authToken = jwtTokenManager.createJWTToken(userUuid); const authToken = jwtTokenManager.createAuthToken(userUuid);
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end( res.end(

View File

@ -22,7 +22,7 @@ import {
import { UserMovesMessage } from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb";
import { TemplatedApp } from "uWebSockets.js"; import { TemplatedApp } from "uWebSockets.js";
import { parse } from "query-string"; import { parse } from "query-string";
import { jwtTokenManager } from "../Services/JWTTokenManager"; import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager"; import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers"; import { emitInBatch } from "../Services/IoSocketHelpers";
@ -173,31 +173,34 @@ export class IoSocketController {
characterLayers = [characterLayers]; characterLayers = [characterLayers];
} }
const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId); const tokenData =
token && typeof token === "string" ? jwtTokenManager.decodeJWTToken(token) : null;
const userIdentifier = tokenData ? tokenData.identifier : "";
let memberTags: string[] = []; let memberTags: string[] = [];
let memberVisitCardUrl: string | null = null; let memberVisitCardUrl: string | null = null;
let memberMessages: unknown; let memberMessages: unknown;
let memberTextures: CharacterTexture[] = []; let memberTextures: CharacterTexture[] = [];
const room = await socketManager.getOrCreateRoom(roomId); const room = await socketManager.getOrCreateRoom(roomId);
let userData: FetchMemberDataByUuidResponse = {
userUuid: userIdentifier,
tags: [],
visitCardUrl: null,
textures: [],
messages: [],
anonymous: true,
};
if (ADMIN_API_URL) { if (ADMIN_API_URL) {
try { try {
let userData: FetchMemberDataByUuidResponse = {
uuid: v4(),
tags: [],
visitCardUrl: null,
textures: [],
messages: [],
anonymous: true,
};
try { try {
userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId); userData = await adminApi.fetchMemberDataByUuid(userIdentifier, roomId, IPAddress);
} catch (err) { } catch (err) {
if (err?.response?.status == 404) { if (err?.response?.status == 404) {
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
console.warn( console.warn(
'Cannot find user with uuid "' + 'Cannot find user with email "' +
userUuid + (userIdentifier || "anonymous") +
'". Performing an anonymous login instead.' '". Performing an anonymous login instead.'
); );
} else if (err?.response?.status == 403) { } else if (err?.response?.status == 403) {
@ -235,7 +238,12 @@ export class IoSocketController {
throw new Error("Use the login URL to connect"); throw new Error("Use the login URL to connect");
} }
} catch (e) { } catch (e) {
console.log("access not granted for user " + userUuid + " and room " + roomId); console.log(
"access not granted for user " +
(userIdentifier || "anonymous") +
" and room " +
roomId
);
console.error(e); console.error(e);
throw new Error("User cannot access this world"); throw new Error("User cannot access this world");
} }
@ -257,7 +265,7 @@ export class IoSocketController {
// Data passed here is accessible on the "websocket" socket object. // Data passed here is accessible on the "websocket" socket object.
url, url,
token, token,
userUuid, userUuid: userData.userUuid,
IPAddress, IPAddress,
roomId, roomId,
name, name,
@ -287,15 +295,10 @@ export class IoSocketController {
context context
); );
} catch (e) { } catch (e) {
/*if (e instanceof Error) { res.upgrade(
console.log(e.message);
res.writeStatus("401 Unauthorized").end(e.message);
} else {
res.writeStatus("500 Internal Server Error").end('An error occurred');
}*/
return res.upgrade(
{ {
rejected: true, rejected: true,
reason: e.reason || null,
message: e.message ? e.message : "500 Internal Server Error", message: e.message ? e.message : "500 Internal Server Error",
}, },
websocketKey, websocketKey,
@ -310,12 +313,14 @@ export class IoSocketController {
open: (ws) => { open: (ws) => {
if (ws.rejected === true) { if (ws.rejected === true) {
//FIX ME to use status code //FIX ME to use status code
if (ws.message === "World is full") { if (ws.reason === tokenInvalidException) {
socketManager.emitTokenExpiredMessage(ws);
} else if (ws.message === "World is full") {
socketManager.emitWorldFullMessage(ws); socketManager.emitWorldFullMessage(ws);
} else { } else {
socketManager.emitConnexionErrorMessage(ws, ws.message as string); socketManager.emitConnexionErrorMessage(ws, ws.message as string);
} }
ws.close(); setTimeout(() => ws.close(), 0);
return; return;
} }

View File

@ -1,11 +1,8 @@
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY"; const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
const API_URL = process.env.API_URL || ""; const API_URL = process.env.API_URL || "";
const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || "") || 600;
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || ""; const JITSI_ISS = process.env.JITSI_ISS || "";
@ -13,14 +10,16 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080; const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080;
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
export const FRONT_URL = process.env.FRONT_URL || "http://localhost";
export const OPID_CLIENT_ID = process.env.OPID_CLIENT_ID || "";
export const OPID_CLIENT_SECRET = process.env.OPID_CLIENT_SECRET || "";
export const OPID_CLIENT_ISSUER = process.env.OPID_CLIENT_ISSUER || "";
export { export {
SECRET_KEY, SECRET_KEY,
MINIMUM_DISTANCE,
API_URL, API_URL,
ADMIN_API_URL, ADMIN_API_URL,
ADMIN_API_TOKEN, ADMIN_API_TOKEN,
MAX_USERS_PER_ROOM,
GROUP_RADIUS,
ALLOW_ARTILLERY, ALLOW_ARTILLERY,
CPU_OVERHEAT_THRESHOLD, CPU_OVERHEAT_THRESHOLD,
JITSI_URL, JITSI_URL,

View File

@ -7,6 +7,7 @@ import { RoomRedirect } from "./AdminApi/RoomRedirect";
export interface AdminApiData { export interface AdminApiData {
roomUrl: string; roomUrl: string;
email: string | null;
mapUrlStart: string; mapUrlStart: string;
tags: string[]; tags: string[];
policy_type: number; policy_type: number;
@ -21,7 +22,7 @@ export interface AdminBannedData {
} }
export interface FetchMemberDataByUuidResponse { export interface FetchMemberDataByUuidResponse {
uuid: string; userUuid: string;
tags: string[]; tags: string[];
visitCardUrl: string | null; visitCardUrl: string | null;
textures: CharacterTexture[]; textures: CharacterTexture[];
@ -46,12 +47,16 @@ class AdminApi {
return res.data; return res.data;
} }
async fetchMemberDataByUuid(uuid: string, roomId: string): Promise<FetchMemberDataByUuidResponse> { async fetchMemberDataByUuid(
userIdentifier: string | null,
roomId: string,
ipAddress: string
): Promise<FetchMemberDataByUuidResponse> {
if (!ADMIN_API_URL) { if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!")); return Promise.reject(new Error("No admin backoffice set!"));
} }
const res = await Axios.get(ADMIN_API_URL + "/api/room/access", { const res = await Axios.get(ADMIN_API_URL + "/api/room/access", {
params: { uuid, roomId }, params: { userIdentifier, roomId, ipAddress },
headers: { Authorization: `${ADMIN_API_TOKEN}` }, headers: { Authorization: `${ADMIN_API_TOKEN}` },
}); });
return res.data; return res.data;
@ -118,6 +123,18 @@ class AdminApi {
return data.data; return data.data;
}); });
} }
async getUrlRoomsFromSameWorld(roomUrl: string): Promise<string[]> {
if (!ADMIN_API_URL) {
return Promise.reject(new Error("No admin backoffice set!"));
}
return Axios.get(ADMIN_API_URL + "/api/room/sameWorld" + "?roomUrl=" + encodeURIComponent(roomUrl), {
headers: { Authorization: `${ADMIN_API_TOKEN}` },
}).then((data) => {
return data.data;
});
}
} }
export const adminApi = new AdminApi(); export const adminApi = new AdminApi();

View File

@ -1,100 +1,25 @@
import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
import { uuid } from "uuidv4"; import { uuid } from "uuidv4";
import Jwt from "jsonwebtoken"; import Jwt, { verify } from "jsonwebtoken";
import { TokenInterface } from "../Controller/AuthenticateController"; import { TokenInterface } from "../Controller/AuthenticateController";
import { adminApi, AdminBannedData } from "../Services/AdminApi"; import { adminApi, AdminBannedData } from "../Services/AdminApi";
export interface AuthTokenData {
identifier: string; //will be a email if logged in or an uuid if anonymous
}
export const tokenInvalidException = "tokenInvalid";
class JWTTokenManager { class JWTTokenManager {
public createJWTToken(userUuid: string) { public createAuthToken(identifier: string) {
return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token return Jwt.sign({ identifier }, SECRET_KEY, { expiresIn: "3d" });
} }
public async getUserUuidFromToken(token: unknown, ipAddress?: string, roomUrl?: string): Promise<string> { public decodeJWTToken(token: string): AuthTokenData {
if (!token) { try {
throw new Error("An authentication error happened, a user tried to connect without a token."); return Jwt.verify(token, SECRET_KEY, { ignoreExpiration: false }) as AuthTokenData;
} catch (e) {
throw { reason: tokenInvalidException, message: e.message };
} }
if (typeof token !== "string") {
throw new Error("Token is expected to be a string");
}
if (token === "test") {
if (ALLOW_ARTILLERY) {
return uuid();
} else {
throw new Error(
"In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"
);
}
}
return new Promise<string>((resolve, reject) => {
Jwt.verify(token, SECRET_KEY, {}, (err, tokenDecoded) => {
const tokenInterface = tokenDecoded as TokenInterface;
if (err) {
console.error("An authentication error happened, invalid JsonWebToken.", err);
reject(new Error("An authentication error happened, invalid JsonWebToken. " + err.message));
return;
}
if (tokenDecoded === undefined) {
console.error("Empty token found.");
reject(new Error("Empty token found."));
return;
}
//verify token
if (!this.isValidToken(tokenInterface)) {
reject(new Error("Authentication error, invalid token structure."));
return;
}
if (ADMIN_API_URL) {
//verify user in admin
let promise = new Promise((resolve) => resolve());
if (ipAddress && roomUrl) {
promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, roomUrl);
}
promise
.then(() => {
adminApi
.fetchCheckUserByToken(tokenInterface.userUuid)
.then(() => {
resolve(tokenInterface.userUuid);
})
.catch((err) => {
//anonymous user
if (err.response && err.response.status && err.response.status === 404) {
resolve(tokenInterface.userUuid);
return;
}
reject(err);
});
})
.catch((err) => {
reject(err);
});
} else {
resolve(tokenInterface.userUuid);
}
});
});
}
private verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string): Promise<AdminBannedData> {
return adminApi
.verifyBanUser(userUuid, ipAddress, roomUrl)
.then((data: AdminBannedData) => {
if (data && data.is_banned) {
throw new Error("User was banned");
}
return data;
})
.catch((err) => {
throw err;
});
}
private isValidToken(token: object): token is TokenInterface {
return !(typeof (token as TokenInterface).userUuid !== "string");
} }
} }

View File

@ -0,0 +1,43 @@
import { Issuer, Client } from "openid-client";
import { OPID_CLIENT_ID, OPID_CLIENT_SECRET, OPID_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable";
const opidRedirectUri = FRONT_URL + "/jwt";
class OpenIDClient {
private issuerPromise: Promise<Client> | null = null;
private initClient(): Promise<Client> {
if (!this.issuerPromise) {
this.issuerPromise = Issuer.discover(OPID_CLIENT_ISSUER).then((issuer) => {
return new issuer.Client({
client_id: OPID_CLIENT_ID,
client_secret: OPID_CLIENT_SECRET,
redirect_uris: [opidRedirectUri],
response_types: ["code"],
});
});
}
return this.issuerPromise;
}
public authorizationUrl(state: string, nonce: string) {
return this.initClient().then((client) => {
return client.authorizationUrl({
scope: "openid email",
prompt: "login",
state: state,
nonce: nonce,
});
});
}
public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string }> {
return this.initClient().then((client) => {
return client.callback(opidRedirectUri, { code }, { nonce }).then((tokenSet) => {
return client.userinfo(tokenSet);
});
});
}
}
export const openIDClient = new OpenIDClient();

View File

@ -1,44 +1,45 @@
import { PusherRoom } from "../Model/PusherRoom"; import { PusherRoom } from "../Model/PusherRoom";
import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface";
import { import {
AdminMessage,
AdminPusherToBackMessage,
AdminRoomMessage,
BanMessage,
CharacterLayerMessage,
EmoteEventMessage,
EmotePromptMessage,
GroupDeleteMessage, GroupDeleteMessage,
ItemEventMessage, ItemEventMessage,
JoinRoomMessage,
PlayGlobalMessage, PlayGlobalMessage,
PusherToBackMessage,
QueryJitsiJwtMessage,
RefreshRoomMessage,
ReportPlayerMessage,
RoomJoinedMessage, RoomJoinedMessage,
SendJitsiJwtMessage,
ServerToAdminClientMessage,
ServerToClientMessage, ServerToClientMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SilentMessage, SilentMessage,
SubMessage, SubMessage,
ReportPlayerMessage, UserJoinedRoomMessage,
UserLeftMessage, UserLeftMessage,
UserLeftRoomMessage,
UserMovesMessage, UserMovesMessage,
ViewportMessage, ViewportMessage,
WebRtcSignalToServerMessage, WebRtcSignalToServerMessage,
QueryJitsiJwtMessage,
SendJitsiJwtMessage,
JoinRoomMessage,
CharacterLayerMessage,
PusherToBackMessage,
WorldFullMessage,
WorldConnexionMessage, WorldConnexionMessage,
AdminPusherToBackMessage, TokenExpiredMessage,
ServerToAdminClientMessage,
EmoteEventMessage,
UserJoinedRoomMessage,
UserLeftRoomMessage,
AdminMessage,
BanMessage,
RefreshRoomMessage,
EmotePromptMessage,
VariableMessage, VariableMessage,
ErrorMessage, ErrorMessage,
WorldFullMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { ADMIN_API_URL, JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, JITSI_ISS, JITSI_URL, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
import { adminApi } from "./AdminApi"; import { adminApi } from "./AdminApi";
import { emitInBatch } from "./IoSocketHelpers"; import { emitInBatch } from "./IoSocketHelpers";
import Jwt from "jsonwebtoken"; import Jwt from "jsonwebtoken";
import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { clientEventsEmitter } from "./ClientEventsEmitter"; import { clientEventsEmitter } from "./ClientEventsEmitter";
import { gaugeManager } from "./GaugeManager"; import { gaugeManager } from "./GaugeManager";
import { apiClientRepository } from "./ApiClientRepository"; import { apiClientRepository } from "./ApiClientRepository";
@ -117,7 +118,7 @@ export class SocketManager implements ZoneEventListener {
console.warn("Admin connection lost to back server"); console.warn("Admin connection lost to back server");
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. // 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, "Admin Connection lost to back server");
} }
console.log("A user left"); console.log("A user left");
}) })
@ -140,24 +141,6 @@ export class SocketManager implements ZoneEventListener {
} }
} }
getAdminSocketDataFor(roomId: string): AdminSocketData {
throw new Error("Not reimplemented yet");
/*const data:AdminSocketData = {
rooms: {},
users: {},
}
const room = this.Worlds.get(roomId);
if (room === undefined) {
return data;
}
const users = room.getUsers();
data.rooms[roomId] = users.size;
users.forEach(user => {
data.users[user.uuid] = true
})
return data;*/
}
async handleJoinRoom(client: ExSocketInterface): Promise<void> { async handleJoinRoom(client: ExSocketInterface): Promise<void> {
const viewport = client.viewport; const viewport = client.viewport;
try { try {
@ -406,17 +389,6 @@ export class SocketManager implements ZoneEventListener {
} }
} }
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
if (!client.tags.includes("admin")) {
//In case of xss injection, we just kill the connection.
throw "Client is not an admin!";
}
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setPlayglobalmessage(playglobalmessage);
client.backConnection.write(pusherToBackMessage);
}
public getWorlds(): Map<string, PusherRoom> { public getWorlds(): Map<string, PusherRoom> {
return this.rooms; return this.rooms;
} }
@ -598,7 +570,20 @@ export class SocketManager implements ZoneEventListener {
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWorldfullmessage(errorMessage); serverToClientMessage.setWorldfullmessage(errorMessage);
client.send(serverToClientMessage.serializeBinary().buffer, true); if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
}
public emitTokenExpiredMessage(client: WebSocket) {
const errorMessage = new TokenExpiredMessage();
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setTokenexpiredmessage(errorMessage);
if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
} }
public emitConnexionErrorMessage(client: WebSocket, message: string) { public emitConnexionErrorMessage(client: WebSocket, message: string) {
@ -625,6 +610,36 @@ export class SocketManager implements ZoneEventListener {
client.backConnection.write(pusherToBackMessage); client.backConnection.write(pusherToBackMessage);
} }
public async emitPlayGlobalMessage(
client: ExSocketInterface,
playGlobalMessageEvent: PlayGlobalMessage
): Promise<void> {
if (!client.tags.includes("admin")) {
throw "Client is not an admin!";
}
const clientRoomUrl = client.roomId;
let tabUrlRooms: string[];
if (playGlobalMessageEvent.getBroadcasttoworld()) {
tabUrlRooms = await adminApi.getUrlRoomsFromSameWorld(clientRoomUrl);
} else {
tabUrlRooms = [clientRoomUrl];
}
const roomMessage = new AdminRoomMessage();
roomMessage.setMessage(playGlobalMessageEvent.getContent());
roomMessage.setType(playGlobalMessageEvent.getType());
for (const roomUrl of tabUrlRooms) {
const apiRoom = await apiClientRepository.getClient(roomUrl);
roomMessage.setRoomid(roomUrl);
apiRoom.sendAdminMessageToRoom(roomMessage, (response) => {
return;
});
}
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();

File diff suppressed because it is too large Load Diff