Adding support to persist variables in Redis
This commit is contained in:
parent
18e4d2ba4e
commit
d955ddfe82
@ -53,6 +53,7 @@
|
|||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
"query-string": "^6.13.3",
|
"query-string": "^6.13.3",
|
||||||
|
"redis": "^3.1.2",
|
||||||
"systeminformation": "^4.31.1",
|
"systeminformation": "^4.31.1",
|
||||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||||
"uuidv4": "^6.0.7"
|
"uuidv4": "^6.0.7"
|
||||||
@ -66,6 +67,7 @@
|
|||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.10",
|
||||||
"@types/jsonwebtoken": "^8.3.8",
|
"@types/jsonwebtoken": "^8.3.8",
|
||||||
"@types/mkdirp": "^1.0.1",
|
"@types/mkdirp": "^1.0.1",
|
||||||
|
"@types/redis": "^2.8.31",
|
||||||
"@types/uuidv4": "^5.0.0",
|
"@types/uuidv4": "^5.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
"@typescript-eslint/parser": "^2.26.0",
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
|
@ -12,6 +12,9 @@ const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
|
|||||||
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
||||||
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
||||||
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 REDIS_HOST = process.env.REDIS_HOST || undefined;
|
||||||
|
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
|
||||||
|
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
|
@ -11,18 +11,19 @@ import {
|
|||||||
EmoteEventMessage,
|
EmoteEventMessage,
|
||||||
JoinRoomMessage,
|
JoinRoomMessage,
|
||||||
SubToPusherRoomMessage,
|
SubToPusherRoomMessage,
|
||||||
VariableMessage, VariableWithTagMessage,
|
VariableMessage,
|
||||||
|
VariableWithTagMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||||
import { Admin } from "../Model/Admin";
|
import { Admin } from "../Model/Admin";
|
||||||
import {adminApi} from "../Services/AdminApi";
|
import { adminApi } from "../Services/AdminApi";
|
||||||
import {isMapDetailsData, MapDetailsData} from "../Services/AdminApi/MapDetailsData";
|
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
||||||
import {ITiledMap} from "@workadventure/tiled-map-type-guard/dist";
|
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
import {mapFetcher} from "../Services/MapFetcher";
|
import { mapFetcher } from "../Services/MapFetcher";
|
||||||
import {VariablesManager} from "../Services/VariablesManager";
|
import { VariablesManager } from "../Services/VariablesManager";
|
||||||
import {ADMIN_API_URL} from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
import {LocalUrlError} from "../Services/LocalUrlError";
|
import { LocalUrlError } from "../Services/LocalUrlError";
|
||||||
|
|
||||||
export type ConnectCallback = (user: User, group: Group) => void;
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
@ -68,10 +69,21 @@ export class GameRoom {
|
|||||||
onMoves: MovesCallback,
|
onMoves: MovesCallback,
|
||||||
onLeaves: LeavesCallback,
|
onLeaves: LeavesCallback,
|
||||||
onEmote: EmoteCallback
|
onEmote: EmoteCallback
|
||||||
) : Promise<GameRoom> {
|
): Promise<GameRoom> {
|
||||||
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
||||||
|
|
||||||
const gameRoom = new GameRoom(roomUrl, mapDetails.mapUrl, connectCallback, disconnectCallback, minDistance, groupRadius, onEnters, onMoves, onLeaves, onEmote);
|
const gameRoom = new GameRoom(
|
||||||
|
roomUrl,
|
||||||
|
mapDetails.mapUrl,
|
||||||
|
connectCallback,
|
||||||
|
disconnectCallback,
|
||||||
|
minDistance,
|
||||||
|
groupRadius,
|
||||||
|
onEnters,
|
||||||
|
onMoves,
|
||||||
|
onLeaves,
|
||||||
|
onEmote
|
||||||
|
);
|
||||||
|
|
||||||
return gameRoom;
|
return gameRoom;
|
||||||
}
|
}
|
||||||
@ -381,7 +393,7 @@ export class GameRoom {
|
|||||||
|
|
||||||
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname);
|
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
console.error('Unexpected room URL', roomUrl);
|
console.error("Unexpected room URL", roomUrl);
|
||||||
throw new Error('Unexpected room URL "' + roomUrl + '"');
|
throw new Error('Unexpected room URL "' + roomUrl + '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,18 +404,18 @@ export class GameRoom {
|
|||||||
policy_type: 1,
|
policy_type: 1,
|
||||||
textures: [],
|
textures: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await adminApi.fetchMapDetails(roomUrl);
|
const result = await adminApi.fetchMapDetails(roomUrl);
|
||||||
if (!isMapDetailsData(result)) {
|
if (!isMapDetailsData(result)) {
|
||||||
console.error('Unexpected room details received from server', result);
|
console.error("Unexpected room details received from server", result);
|
||||||
throw new Error('Unexpected room details received from server');
|
throw new Error("Unexpected room details received from server");
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapPromise: Promise<ITiledMap>|undefined;
|
private mapPromise: Promise<ITiledMap> | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a promise to the map file.
|
* Returns a promise to the map file.
|
||||||
@ -418,27 +430,45 @@ export class GameRoom {
|
|||||||
return this.mapPromise;
|
return this.mapPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private variableManagerPromise: Promise<VariablesManager>|undefined;
|
private variableManagerPromise: Promise<VariablesManager> | undefined;
|
||||||
|
|
||||||
private getVariableManager(): Promise<VariablesManager> {
|
private getVariableManager(): Promise<VariablesManager> {
|
||||||
if (!this.variableManagerPromise) {
|
if (!this.variableManagerPromise) {
|
||||||
this.variableManagerPromise = new Promise<VariablesManager>((resolve, reject) => {
|
this.variableManagerPromise = new Promise<VariablesManager>((resolve, reject) => {
|
||||||
this.getMap().then((map) => {
|
this.getMap()
|
||||||
resolve(new VariablesManager(map));
|
.then((map) => {
|
||||||
}).catch(e => {
|
const variablesManager = new VariablesManager(this.roomUrl, map);
|
||||||
if (e instanceof LocalUrlError) {
|
variablesManager
|
||||||
// If we are trying to load a local URL, we are probably in test mode.
|
.init()
|
||||||
// In this case, let's bypass the server-side checks completely.
|
.then(() => {
|
||||||
|
resolve(variablesManager);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof LocalUrlError) {
|
||||||
|
// If we are trying to load a local URL, we are probably in test mode.
|
||||||
|
// In this case, let's bypass the server-side checks completely.
|
||||||
|
|
||||||
// FIXME: find a way to send a warning to the client side
|
// FIXME: find a way to send a warning to the client side
|
||||||
// FIXME: find a way to send a warning to the client side
|
// FIXME: find a way to send a warning to the client side
|
||||||
// FIXME: find a way to send a warning to the client side
|
// FIXME: find a way to send a warning to the client side
|
||||||
// FIXME: find a way to send a warning to the client side
|
// FIXME: find a way to send a warning to the client side
|
||||||
resolve(new VariablesManager(null));
|
const variablesManager = new VariablesManager(this.roomUrl, null);
|
||||||
} else {
|
variablesManager
|
||||||
reject(e);
|
.init()
|
||||||
}
|
.then(() => {
|
||||||
})
|
resolve(variablesManager);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this.variableManagerPromise;
|
return this.variableManagerPromise;
|
||||||
|
@ -4,7 +4,9 @@ import {
|
|||||||
AdminMessage,
|
AdminMessage,
|
||||||
AdminPusherToBackMessage,
|
AdminPusherToBackMessage,
|
||||||
AdminRoomMessage,
|
AdminRoomMessage,
|
||||||
BanMessage, BatchToPusherMessage, BatchToPusherRoomMessage,
|
BanMessage,
|
||||||
|
BatchToPusherMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
EmptyMessage,
|
EmptyMessage,
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
@ -12,17 +14,19 @@ import {
|
|||||||
PlayGlobalMessage,
|
PlayGlobalMessage,
|
||||||
PusherToBackMessage,
|
PusherToBackMessage,
|
||||||
QueryJitsiJwtMessage,
|
QueryJitsiJwtMessage,
|
||||||
RefreshRoomPromptMessage, RoomMessage,
|
RefreshRoomPromptMessage,
|
||||||
|
RoomMessage,
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
SilentMessage,
|
SilentMessage,
|
||||||
UserMovesMessage, VariableMessage,
|
UserMovesMessage,
|
||||||
|
VariableMessage,
|
||||||
WebRtcSignalToServerMessage,
|
WebRtcSignalToServerMessage,
|
||||||
WorldFullWarningToRoomMessage,
|
WorldFullWarningToRoomMessage,
|
||||||
ZoneMessage,
|
ZoneMessage,
|
||||||
} from "./Messages/generated/messages_pb";
|
} from "./Messages/generated/messages_pb";
|
||||||
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
||||||
import { socketManager } from "./Services/SocketManager";
|
import { socketManager } from "./Services/SocketManager";
|
||||||
import {emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket} from "./Services/MessageHelpers";
|
import { emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket } from "./Services/MessageHelpers";
|
||||||
import { User, UserSocket } from "./Model/User";
|
import { User, UserSocket } from "./Model/User";
|
||||||
import { GameRoom } from "./Model/GameRoom";
|
import { GameRoom } from "./Model/GameRoom";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
@ -55,7 +59,8 @@ const roomManager: IRoomManagerServer = {
|
|||||||
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
||||||
socketManager.leaveRoom(gameRoom, myUser);
|
socketManager.leaveRoom(gameRoom, myUser);
|
||||||
}
|
}
|
||||||
}).catch(e => emitError(call, e));
|
})
|
||||||
|
.catch((e) => emitError(call, e));
|
||||||
} else {
|
} else {
|
||||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||||
}
|
}
|
||||||
@ -138,22 +143,30 @@ const roomManager: IRoomManagerServer = {
|
|||||||
debug("listenZone called");
|
debug("listenZone called");
|
||||||
const zoneMessage = call.request;
|
const zoneMessage = call.request;
|
||||||
|
|
||||||
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()).catch(e => {
|
socketManager
|
||||||
emitErrorOnZoneSocket(call, e.toString());
|
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
});
|
.catch((e) => {
|
||||||
|
emitErrorOnZoneSocket(call, e.toString());
|
||||||
|
});
|
||||||
|
|
||||||
call.on("cancelled", () => {
|
call.on("cancelled", () => {
|
||||||
debug("listenZone cancelled");
|
debug("listenZone cancelled");
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()).catch(e => console.error(e));
|
socketManager
|
||||||
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("close", () => {
|
call.on("close", () => {
|
||||||
debug("listenZone connection closed");
|
debug("listenZone connection closed");
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()).catch(e => console.error(e));
|
socketManager
|
||||||
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
}).on("error", (e) => {
|
}).on("error", (e) => {
|
||||||
console.error("An error occurred in listenZone stream:", e);
|
console.error("An error occurred in listenZone stream:", e);
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()).catch(e => console.error(e));
|
socketManager
|
||||||
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -162,25 +175,24 @@ const roomManager: IRoomManagerServer = {
|
|||||||
debug("listenRoom called");
|
debug("listenRoom called");
|
||||||
const roomMessage = call.request;
|
const roomMessage = call.request;
|
||||||
|
|
||||||
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch(e => {
|
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
|
||||||
emitErrorOnRoomSocket(call, e.toString());
|
emitErrorOnRoomSocket(call, e.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("cancelled", () => {
|
call.on("cancelled", () => {
|
||||||
debug("listenRoom cancelled");
|
debug("listenRoom cancelled");
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch(e => console.error(e));
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on("close", () => {
|
call.on("close", () => {
|
||||||
debug("listenRoom connection closed");
|
debug("listenRoom connection closed");
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch(e => console.error(e));
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
}).on("error", (e) => {
|
}).on("error", (e) => {
|
||||||
console.error("An error occurred in listenRoom stream:", e);
|
console.error("An error occurred in listenRoom stream:", e);
|
||||||
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch(e => console.error(e));
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
adminRoom(call: AdminSocket): void {
|
adminRoom(call: AdminSocket): void {
|
||||||
@ -194,9 +206,12 @@ const roomManager: IRoomManagerServer = {
|
|||||||
if (room === null) {
|
if (room === null) {
|
||||||
if (message.hasSubscribetoroom()) {
|
if (message.hasSubscribetoroom()) {
|
||||||
const roomId = message.getSubscribetoroom();
|
const roomId = message.getSubscribetoroom();
|
||||||
socketManager.handleJoinAdminRoom(admin, roomId).then((gameRoom: GameRoom) => {
|
socketManager
|
||||||
room = gameRoom;
|
.handleJoinAdminRoom(admin, roomId)
|
||||||
}).catch(e => console.error(e));
|
.then((gameRoom: GameRoom) => {
|
||||||
|
room = gameRoom;
|
||||||
|
})
|
||||||
|
.catch((e) => console.error(e));
|
||||||
} else {
|
} else {
|
||||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||||
}
|
}
|
||||||
@ -221,11 +236,9 @@ const roomManager: IRoomManagerServer = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
socketManager.sendAdminMessage(
|
socketManager
|
||||||
call.request.getRoomid(),
|
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||||
call.request.getRecipientuuid(),
|
.catch((e) => console.error(e));
|
||||||
call.request.getMessage()
|
|
||||||
).catch(e => console.error(e));
|
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
@ -236,13 +249,17 @@ const roomManager: IRoomManagerServer = {
|
|||||||
},
|
},
|
||||||
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
// FIXME Work in progress
|
// FIXME Work in progress
|
||||||
socketManager.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage()).catch(e => console.error(e));
|
socketManager
|
||||||
|
.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
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.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()).catch(e => console.error(e));
|
socketManager
|
||||||
|
.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendWorldFullWarningToRoom(
|
sendWorldFullWarningToRoom(
|
||||||
@ -250,7 +267,7 @@ const roomManager: IRoomManagerServer = {
|
|||||||
callback: sendUnaryData<EmptyMessage>
|
callback: sendUnaryData<EmptyMessage>
|
||||||
): void {
|
): 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.dispatchWorldFullWarning(call.request.getRoomid()).catch(e => console.error(e));
|
socketManager.dispatchWorldFullWarning(call.request.getRoomid()).catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendRefreshRoomPrompt(
|
sendRefreshRoomPrompt(
|
||||||
@ -258,9 +275,9 @@ const roomManager: IRoomManagerServer = {
|
|||||||
callback: sendUnaryData<EmptyMessage>
|
callback: sendUnaryData<EmptyMessage>
|
||||||
): void {
|
): 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.dispatchRoomRefresh(call.request.getRoomid()).catch(e => console.error(e));
|
socketManager.dispatchRoomRefresh(call.request.getRoomid()).catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { roomManager };
|
export { roomManager };
|
||||||
|
@ -15,7 +15,7 @@ export const isMapDetailsData = new tg.IsInterface()
|
|||||||
textures: tg.isArray(isCharacterTexture),
|
textures: tg.isArray(isCharacterTexture),
|
||||||
})
|
})
|
||||||
.withOptionalProperties({
|
.withOptionalProperties({
|
||||||
roomSlug: tg.isUnion(tg.isString, tg.isNull), // deprecated
|
roomSlug: tg.isUnion(tg.isString, tg.isNull), // deprecated
|
||||||
})
|
})
|
||||||
.get();
|
.get();
|
||||||
export type MapDetailsData = tg.GuardedType<typeof isMapDetailsData>;
|
export type MapDetailsData = tg.GuardedType<typeof isMapDetailsData>;
|
||||||
|
@ -1,2 +1 @@
|
|||||||
export class LocalUrlError extends Error {
|
export class LocalUrlError extends Error {}
|
||||||
}
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import ipaddr from 'ipaddr.js';
|
import ipaddr from "ipaddr.js";
|
||||||
import { Resolver } from 'dns';
|
import { Resolver } from "dns";
|
||||||
import { promisify } from 'util';
|
import { promisify } from "util";
|
||||||
import {LocalUrlError} from "./LocalUrlError";
|
import { LocalUrlError } from "./LocalUrlError";
|
||||||
import {ITiledMap} from "@workadventure/tiled-map-type-guard";
|
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
|
||||||
import {isTiledMap} from "@workadventure/tiled-map-type-guard/dist";
|
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
|
|
||||||
class MapFetcher {
|
class MapFetcher {
|
||||||
async fetchMap(mapUrl: string): Promise<ITiledMap> {
|
async fetchMap(mapUrl: string): Promise<ITiledMap> {
|
||||||
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
|
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
|
||||||
|
|
||||||
if (await this.isLocalUrl(mapUrl)) {
|
if (await this.isLocalUrl(mapUrl)) {
|
||||||
throw new LocalUrlError('URL for map "'+mapUrl+'" targets a local map');
|
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
|
// Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
|
||||||
@ -22,12 +22,12 @@ class MapFetcher {
|
|||||||
// - We make sure we are only passing "GET" requests
|
// - We make sure we are only passing "GET" requests
|
||||||
// - The result of the query is never displayed to the end user
|
// - The result of the query is never displayed to the end user
|
||||||
const res = await Axios.get(mapUrl, {
|
const res = await Axios.get(mapUrl, {
|
||||||
maxContentLength: 50*1024*1024, // Max content length: 50MB. Maps should not be bigger
|
maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
|
||||||
timeout: 10000, // Timeout after 10 seconds
|
timeout: 10000, // Timeout after 10 seconds
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isTiledMap(res.data)) {
|
if (!isTiledMap(res.data)) {
|
||||||
throw new Error('Invalid map format for map '+mapUrl);
|
throw new Error("Invalid map format for map " + mapUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.data;
|
return res.data;
|
||||||
@ -39,7 +39,7 @@ class MapFetcher {
|
|||||||
*/
|
*/
|
||||||
private async isLocalUrl(url: string): Promise<boolean> {
|
private async isLocalUrl(url: string): Promise<boolean> {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
if (urlObj.hostname === 'localhost' || urlObj.hostname.endsWith('.localhost')) {
|
if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class MapFetcher {
|
|||||||
|
|
||||||
for (const address of addresses) {
|
for (const address of addresses) {
|
||||||
const addr = ipaddr.parse(address);
|
const addr = ipaddr.parse(address);
|
||||||
if (addr.range() !== 'unicast') {
|
if (addr.range() !== "unicast") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
BatchMessage,
|
BatchMessage,
|
||||||
BatchToPusherMessage, BatchToPusherRoomMessage,
|
BatchToPusherMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
ServerToClientMessage, SubToPusherMessage, SubToPusherRoomMessage
|
ServerToClientMessage,
|
||||||
|
SubToPusherMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { UserSocket } from "_Model/User";
|
import { UserSocket } from "_Model/User";
|
||||||
import {RoomSocket, ZoneSocket} from "../RoomManager";
|
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||||
|
|
||||||
export function emitError(Client: UserSocket, message: string): void {
|
export function emitError(Client: UserSocket, message: string): void {
|
||||||
const errorMessage = new ErrorMessage();
|
const errorMessage = new ErrorMessage();
|
||||||
|
23
back/src/Services/RedisClient.ts
Normal file
23
back/src/Services/RedisClient.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ClientOpts, createClient, RedisClient } from "redis";
|
||||||
|
import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
let redisClient: RedisClient | null = null;
|
||||||
|
|
||||||
|
if (REDIS_HOST !== undefined) {
|
||||||
|
const config: ClientOpts = {
|
||||||
|
host: REDIS_HOST,
|
||||||
|
port: REDIS_PORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (REDIS_PASSWORD) {
|
||||||
|
config.password = REDIS_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
redisClient = createClient(config);
|
||||||
|
|
||||||
|
redisClient.on("error", (err) => {
|
||||||
|
console.error("Error connecting to Redis:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { redisClient };
|
40
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
40
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { promisify } from "util";
|
||||||
|
import { RedisClient } from "redis";
|
||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class in charge of saving/loading variables from the data store
|
||||||
|
*/
|
||||||
|
export class RedisVariablesRepository implements VariablesRepositoryInterface {
|
||||||
|
private readonly hgetall: OmitThisParameter<(arg1: string) => Promise<{ [p: string]: string }>>;
|
||||||
|
private readonly hset: OmitThisParameter<(arg1: [string, ...string[]]) => Promise<number>>;
|
||||||
|
|
||||||
|
constructor(redisClient: RedisClient) {
|
||||||
|
// @eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
this.hgetall = promisify(redisClient.hgetall).bind(redisClient);
|
||||||
|
// @eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
this.hset = promisify(redisClient.hset).bind(redisClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all variables for a room.
|
||||||
|
*
|
||||||
|
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
||||||
|
*/
|
||||||
|
async loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
||||||
|
return this.hgetall(roomUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
||||||
|
// TODO: handle the case for "undefined"
|
||||||
|
// TODO: handle the case for "undefined"
|
||||||
|
// TODO: handle the case for "undefined"
|
||||||
|
// TODO: handle the case for "undefined"
|
||||||
|
// TODO: handle the case for "undefined"
|
||||||
|
|
||||||
|
// TODO: SLOW WRITING EVERY 2 SECONDS WITH A TIMEOUT
|
||||||
|
|
||||||
|
// @ts-ignore See https://stackoverflow.com/questions/63539317/how-do-i-use-hmset-with-node-promisify
|
||||||
|
return this.hset(roomUrl, key, value);
|
||||||
|
}
|
||||||
|
}
|
14
back/src/Services/Repository/VariablesRepository.ts
Normal file
14
back/src/Services/Repository/VariablesRepository.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { RedisVariablesRepository } from "./RedisVariablesRepository";
|
||||||
|
import { redisClient } from "../RedisClient";
|
||||||
|
import { VoidVariablesRepository } from "./VoidVariablesRepository";
|
||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
let variablesRepository: VariablesRepositoryInterface;
|
||||||
|
if (!redisClient) {
|
||||||
|
console.warn("WARNING: Redis isnot configured. No variables will be persisted.");
|
||||||
|
variablesRepository = new VoidVariablesRepository();
|
||||||
|
} else {
|
||||||
|
variablesRepository = new RedisVariablesRepository(redisClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { variablesRepository };
|
10
back/src/Services/Repository/VariablesRepositoryInterface.ts
Normal file
10
back/src/Services/Repository/VariablesRepositoryInterface.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface VariablesRepositoryInterface {
|
||||||
|
/**
|
||||||
|
* Load all variables for a room.
|
||||||
|
*
|
||||||
|
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
||||||
|
*/
|
||||||
|
loadVariables(roomUrl: string): Promise<{ [key: string]: string }>;
|
||||||
|
|
||||||
|
saveVariable(roomUrl: string, key: string, value: string): Promise<number>;
|
||||||
|
}
|
14
back/src/Services/Repository/VoidVariablesRepository.ts
Normal file
14
back/src/Services/Repository/VoidVariablesRepository.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock class in charge of NOT saving/loading variables from the data store
|
||||||
|
*/
|
||||||
|
export class VoidVariablesRepository implements VariablesRepositoryInterface {
|
||||||
|
loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
||||||
|
return Promise.resolve(0);
|
||||||
|
}
|
||||||
|
}
|
@ -305,13 +305,15 @@ export class SocketManager {
|
|||||||
this.onClientLeave(thing, newZone, listener),
|
this.onClientLeave(thing, newZone, listener),
|
||||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||||
this.onEmote(emoteEventMessage, listener)
|
this.onEmote(emoteEventMessage, listener)
|
||||||
).then((gameRoom) => {
|
)
|
||||||
gaugeManager.incNbRoomGauge();
|
.then((gameRoom) => {
|
||||||
resolve(gameRoom);
|
gaugeManager.incNbRoomGauge();
|
||||||
}).catch((e) => {
|
resolve(gameRoom);
|
||||||
this.roomsPromises.delete(roomId);
|
})
|
||||||
reject(e);
|
.catch((e) => {
|
||||||
});
|
this.roomsPromises.delete(roomId);
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
this.roomsPromises.set(roomId, roomPromise);
|
this.roomsPromises.set(roomId, roomPromise);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* Handles variables shared between the scripting API and the server.
|
* Handles variables shared between the scripting API and the server.
|
||||||
*/
|
*/
|
||||||
import {ITiledMap, ITiledMapObject, ITiledMapObjectLayer} from "@workadventure/tiled-map-type-guard/dist";
|
import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
import {User} from "_Model/User";
|
import { User } from "_Model/User";
|
||||||
|
import { variablesRepository } from "./Repository/VariablesRepository";
|
||||||
|
import { redisClient } from "./RedisClient";
|
||||||
|
|
||||||
interface Variable {
|
interface Variable {
|
||||||
defaultValue?: string,
|
defaultValue?: string;
|
||||||
persist?: boolean,
|
persist?: boolean;
|
||||||
readableBy?: string,
|
readableBy?: string;
|
||||||
writableBy?: string,
|
writableBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VariablesManager {
|
export class VariablesManager {
|
||||||
@ -25,7 +27,7 @@ export class VariablesManager {
|
|||||||
/**
|
/**
|
||||||
* @param map The map can be "null" if it is hosted on a private network. In this case, we assume this is a test setup and bypass any server-side checks.
|
* @param map The map can be "null" if it is hosted on a private network. In this case, we assume this is a test setup and bypass any server-side checks.
|
||||||
*/
|
*/
|
||||||
constructor(private map: ITiledMap | null) {
|
constructor(private roomUrl: string, private map: ITiledMap | null) {
|
||||||
// We initialize the list of variable object at room start. The objects cannot be edited later
|
// We initialize the list of variable object at room start. The objects cannot be edited later
|
||||||
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
||||||
if (map) {
|
if (map) {
|
||||||
@ -40,14 +42,43 @@ export class VariablesManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let's load data from the Redis backend.
|
||||||
|
*/
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
if (!this.shouldPersist()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const variables = await variablesRepository.loadVariables(this.roomUrl);
|
||||||
|
console.error("LIST OF VARIABLES FETCHED", variables);
|
||||||
|
for (const key in variables) {
|
||||||
|
this._variables.set(key, variables[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if saving should be enabled, and false otherwise.
|
||||||
|
*
|
||||||
|
* Saving is enabled if REDIS_HOST is set
|
||||||
|
* unless we are editing a local map
|
||||||
|
* unless we are in dev mode in which case it is ok to save
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private shouldPersist(): boolean {
|
||||||
|
return redisClient !== null && (this.map !== null || process.env.NODE_ENV === "development");
|
||||||
|
}
|
||||||
|
|
||||||
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
|
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
|
||||||
const objects = new Map<string, Variable>();
|
const objects = new Map<string, Variable>();
|
||||||
for (const layer of map.layers) {
|
for (const layer of map.layers) {
|
||||||
if (layer.type === 'objectgroup') {
|
if (layer.type === "objectgroup") {
|
||||||
for (const object of (layer as ITiledMapObjectLayer).objects) {
|
for (const object of (layer as ITiledMapObjectLayer).objects) {
|
||||||
if (object.type === 'variable') {
|
if (object.type === "variable") {
|
||||||
if (object.template) {
|
if (object.template) {
|
||||||
console.warn('Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.')
|
console.warn(
|
||||||
|
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,26 +98,30 @@ export class VariablesManager {
|
|||||||
for (const property of object.properties) {
|
for (const property of object.properties) {
|
||||||
const value = property.value;
|
const value = property.value;
|
||||||
switch (property.name) {
|
switch (property.name) {
|
||||||
case 'default':
|
case "default":
|
||||||
variable.defaultValue = JSON.stringify(value);
|
variable.defaultValue = JSON.stringify(value);
|
||||||
break;
|
break;
|
||||||
case 'persist':
|
case "persist":
|
||||||
if (typeof value !== 'boolean') {
|
if (typeof value !== "boolean") {
|
||||||
throw new Error('The persist property of variable "' + object.name + '" must be a boolean');
|
throw new Error('The persist property of variable "' + object.name + '" must be a boolean');
|
||||||
}
|
}
|
||||||
variable.persist = value;
|
variable.persist = value;
|
||||||
break;
|
break;
|
||||||
case 'writableBy':
|
case "writableBy":
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== "string") {
|
||||||
throw new Error('The writableBy property of variable "' + object.name + '" must be a string');
|
throw new Error(
|
||||||
|
'The writableBy property of variable "' + object.name + '" must be a string'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
variable.writableBy = value;
|
variable.writableBy = value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'readableBy':
|
case "readableBy":
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== "string") {
|
||||||
throw new Error('The readableBy property of variable "' + object.name + '" must be a string');
|
throw new Error(
|
||||||
|
'The readableBy property of variable "' + object.name + '" must be a string'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
variable.readableBy = value;
|
variable.readableBy = value;
|
||||||
@ -107,14 +142,27 @@ export class VariablesManager {
|
|||||||
throw new Error('Trying to set a variable "' + name + '" that is not defined as an object in the map.');
|
throw new Error('Trying to set a variable "' + name + '" that is not defined as an object in the map.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variableObject.writableBy && user.tags.indexOf(variableObject.writableBy) === -1) {
|
if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) {
|
||||||
throw new Error('Trying to set a variable "' + name + '". User "' + user.name + '" does not have sufficient permission. Required tag: "' + variableObject.writableBy + '". User tags: ' + user.tags.join(', ') + ".");
|
throw new Error(
|
||||||
|
'Trying to set a variable "' +
|
||||||
|
name +
|
||||||
|
'". User "' +
|
||||||
|
user.name +
|
||||||
|
'" does not have sufficient permission. Required tag: "' +
|
||||||
|
variableObject.writableBy +
|
||||||
|
'". User tags: ' +
|
||||||
|
user.tags.join(", ") +
|
||||||
|
"."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
readableBy = variableObject.readableBy;
|
readableBy = variableObject.readableBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._variables.set(name, value);
|
this._variables.set(name, value);
|
||||||
|
variablesRepository
|
||||||
|
.saveVariable(this.roomUrl, name, value)
|
||||||
|
.catch((e) => console.error("Error while saving variable in Redis:", e));
|
||||||
return readableBy;
|
return readableBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,9 +176,9 @@ export class VariablesManager {
|
|||||||
for (const [key, value] of this._variables.entries()) {
|
for (const [key, value] of this._variables.entries()) {
|
||||||
const variableObject = this.variableObjects.get(key);
|
const variableObject = this.variableObjects.get(key);
|
||||||
if (variableObject === undefined) {
|
if (variableObject === undefined) {
|
||||||
throw new Error('Unexpected variable "'+key+'" found has no associated variableObject.');
|
throw new Error('Unexpected variable "' + key + '" found has no associated variableObject.');
|
||||||
}
|
}
|
||||||
if (!variableObject.readableBy || tags.indexOf(variableObject.readableBy) !== -1) {
|
if (!variableObject.readableBy || tags.includes(variableObject.readableBy)) {
|
||||||
readableVariables.set(key, value);
|
readableVariables.set(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMess
|
|||||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
|
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
|
||||||
|
|
||||||
describe("GameRoom", () => {
|
describe("GameRoom", () => {
|
||||||
it("should connect user1 and user2", () => {
|
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++;
|
||||||
@ -47,8 +47,7 @@ describe("GameRoom", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const world = new GameRoom('_/global/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));
|
||||||
@ -67,7 +66,7 @@ describe("GameRoom", () => {
|
|||||||
expect(connectCalledNumber).toBe(2);
|
expect(connectCalledNumber).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should connect 3 users", () => {
|
it("should connect 3 users", async () => {
|
||||||
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;
|
||||||
@ -76,7 +75,7 @@ describe("GameRoom", () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const world = new GameRoom('_/global/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));
|
||||||
|
|
||||||
@ -95,7 +94,7 @@ describe("GameRoom", () => {
|
|||||||
expect(connectCalled).toBe(true);
|
expect(connectCalled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should disconnect user1 and user2", () => {
|
it("should disconnect user1 and user2", async () => {
|
||||||
let connectCalled: boolean = false;
|
let connectCalled: boolean = false;
|
||||||
let disconnectCallNumber: number = 0;
|
let disconnectCallNumber: number = 0;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
@ -105,7 +104,7 @@ describe("GameRoom", () => {
|
|||||||
disconnectCallNumber++;
|
disconnectCallNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const world = new GameRoom('_/global/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));
|
||||||
|
|
||||||
|
@ -122,6 +122,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
|
"@types/redis@^2.8.31":
|
||||||
|
version "2.8.31"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.31.tgz#c11c1b269fec132ac2ec9eb891edf72fc549149e"
|
||||||
|
integrity sha512-daWrrTDYaa5iSDFbgzZ9gOOzyp2AJmYK59OlG/2KGBgYWF3lfs8GDKm1c//tik5Uc93hDD36O+qLPvzDolChbA==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/strip-bom@^3.0.0":
|
"@types/strip-bom@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
|
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
|
||||||
@ -804,6 +811,11 @@ delegates@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||||
|
|
||||||
|
denque@^1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de"
|
||||||
|
integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==
|
||||||
|
|
||||||
detect-libc@^1.0.2:
|
detect-libc@^1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||||
@ -2441,6 +2453,33 @@ redent@^1.0.0:
|
|||||||
indent-string "^2.1.0"
|
indent-string "^2.1.0"
|
||||||
strip-indent "^1.0.1"
|
strip-indent "^1.0.1"
|
||||||
|
|
||||||
|
redis-commands@^1.7.0:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
|
||||||
|
integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
|
||||||
|
|
||||||
|
redis-errors@^1.0.0, redis-errors@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
|
||||||
|
integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
|
||||||
|
|
||||||
|
redis-parser@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
|
||||||
|
integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
|
||||||
|
dependencies:
|
||||||
|
redis-errors "^1.0.0"
|
||||||
|
|
||||||
|
redis@^3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c"
|
||||||
|
integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==
|
||||||
|
dependencies:
|
||||||
|
denque "^1.5.0"
|
||||||
|
redis-commands "^1.7.0"
|
||||||
|
redis-errors "^1.2.0"
|
||||||
|
redis-parser "^3.0.0"
|
||||||
|
|
||||||
regex-not@^1.0.0, regex-not@^1.0.2:
|
regex-not@^1.0.0, regex-not@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
|
"REDIS_HOST": "redis",
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {})
|
} else {})
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
|
"REDIS_HOST": "redis",
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {})
|
} else {})
|
||||||
@ -97,6 +99,9 @@
|
|||||||
},
|
},
|
||||||
"ports": [80]
|
"ports": [80]
|
||||||
},
|
},
|
||||||
|
"redis": {
|
||||||
|
"image": "redis:6",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
k8sextension(k8sConf)::
|
k8sextension(k8sConf)::
|
||||||
|
@ -120,6 +120,8 @@ services:
|
|||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
||||||
|
REDIS_HOST: redis
|
||||||
|
NODE_ENV: development
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
@ -168,6 +170,9 @@ services:
|
|||||||
- ./front:/usr/src/front
|
- ./front:/usr/src/front
|
||||||
- ./pusher:/usr/src/pusher
|
- ./pusher:/usr/src/pusher
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
|
||||||
# coturn:
|
# coturn:
|
||||||
# image: coturn/coturn:4.5.2
|
# image: coturn/coturn:4.5.2
|
||||||
# command:
|
# command:
|
||||||
|
@ -115,6 +115,8 @@ services:
|
|||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
||||||
MAX_PER_GROUP: "MAX_PER_GROUP"
|
MAX_PER_GROUP: "MAX_PER_GROUP"
|
||||||
|
REDIS_HOST: redis
|
||||||
|
NODE_ENV: development
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
@ -157,6 +159,20 @@ services:
|
|||||||
- ./front:/usr/src/front
|
- ./front:/usr/src/front
|
||||||
- ./pusher:/usr/src/pusher
|
- ./pusher:/usr/src/pusher
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
|
||||||
|
redisinsight:
|
||||||
|
image: redislabs/redisinsight:latest
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.redisinsight.entryPoints=web"
|
||||||
|
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.rule=Host(`redis.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.service=redisinsight"
|
||||||
|
|
||||||
# coturn:
|
# coturn:
|
||||||
# image: coturn/coturn:4.5.2
|
# image: coturn/coturn:4.5.2
|
||||||
# command:
|
# command:
|
||||||
|
@ -82,6 +82,8 @@ The object **type** MUST be **variable**.
|
|||||||
|
|
||||||
You can set a default value for the object in the `default` property.
|
You can set a default value for the object in the `default` property.
|
||||||
|
|
||||||
|
#### Persisting variables state
|
||||||
|
|
||||||
Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay
|
Use the `persist` property to save the state of the variable in database. If `persist` is false, the variable will stay
|
||||||
in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the
|
in the memory of the WorkAdventure servers but will be wiped out of the memory as soon as the room is empty (or if the
|
||||||
server restarts).
|
server restarts).
|
||||||
@ -89,11 +91,13 @@ server restarts).
|
|||||||
{.alert.alert-info}
|
{.alert.alert-info}
|
||||||
Do not use `persist` for highly dynamic values that have a short life spawn.
|
Do not use `persist` for highly dynamic values that have a short life spawn.
|
||||||
|
|
||||||
|
#### Managing access rights to variables
|
||||||
|
|
||||||
With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string
|
With `readableBy` and `writableBy`, you control who can read of write in this variable. The property accepts a string
|
||||||
representing a "tag". Anyone having this "tag" can read/write in the variable.
|
representing a "tag". Anyone having this "tag" can read/write in the variable.
|
||||||
|
|
||||||
{.alert.alert-warning}
|
{.alert.alert-warning}
|
||||||
`readableBy` and `writableBy` are specific to the public version of WorkAdventure because the notion of tags
|
`readableBy` and `writableBy` are specific to the "online" version of WorkAdventure because the notion of tags
|
||||||
is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure).
|
is not available unless you have an "admin" server (that is not part of the self-hosted version of WorkAdventure).
|
||||||
|
|
||||||
Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable.
|
Finally, the `jsonSchema` property can contain [a complete JSON schema](https://json-schema.org/) to validate the content of the variable.
|
||||||
|
@ -7,7 +7,8 @@ import { apiClientRepository } from "../Services/ApiClientRepository";
|
|||||||
import {
|
import {
|
||||||
BatchToPusherMessage,
|
BatchToPusherMessage,
|
||||||
BatchToPusherRoomMessage,
|
BatchToPusherRoomMessage,
|
||||||
EmoteEventMessage, ErrorMessage,
|
EmoteEventMessage,
|
||||||
|
ErrorMessage,
|
||||||
GroupLeftZoneMessage,
|
GroupLeftZoneMessage,
|
||||||
GroupUpdateZoneMessage,
|
GroupUpdateZoneMessage,
|
||||||
RoomMessage,
|
RoomMessage,
|
||||||
@ -15,7 +16,8 @@ import {
|
|||||||
UserJoinedZoneMessage,
|
UserJoinedZoneMessage,
|
||||||
UserLeftZoneMessage,
|
UserLeftZoneMessage,
|
||||||
UserMovedMessage,
|
UserMovedMessage,
|
||||||
VariableMessage, VariableWithTagMessage,
|
VariableMessage,
|
||||||
|
VariableWithTagMessage,
|
||||||
ZoneMessage,
|
ZoneMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
@ -99,7 +101,7 @@ export class PusherRoom {
|
|||||||
// Let's dispatch this variable to all the listeners
|
// Let's dispatch this variable to all the listeners
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
const subMessage = new SubMessage();
|
const subMessage = new SubMessage();
|
||||||
if (!readableBy || listener.tags.indexOf(readableBy) !== -1) {
|
if (!readableBy || listener.tags.includes(readableBy)) {
|
||||||
subMessage.setVariablemessage(variableMessage);
|
subMessage.setVariablemessage(variableMessage);
|
||||||
}
|
}
|
||||||
listener.emitInBatch(subMessage);
|
listener.emitInBatch(subMessage);
|
||||||
|
@ -14,7 +14,8 @@ import {
|
|||||||
UserMovedMessage,
|
UserMovedMessage,
|
||||||
ZoneMessage,
|
ZoneMessage,
|
||||||
EmoteEventMessage,
|
EmoteEventMessage,
|
||||||
CompanionMessage, ErrorMessage,
|
CompanionMessage,
|
||||||
|
ErrorMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ClientReadableStream } from "grpc";
|
import { ClientReadableStream } from "grpc";
|
||||||
import { PositionDispatcher } from "_Model/PositionDispatcher";
|
import { PositionDispatcher } from "_Model/PositionDispatcher";
|
||||||
|
@ -30,7 +30,8 @@ import {
|
|||||||
BanMessage,
|
BanMessage,
|
||||||
RefreshRoomMessage,
|
RefreshRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
VariableMessage, ErrorMessage,
|
VariableMessage,
|
||||||
|
ErrorMessage,
|
||||||
} 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, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
||||||
|
Loading…
Reference in New Issue
Block a user