Adding support to persist variables in Redis
This commit is contained in:
parent
18e4d2ba4e
commit
d955ddfe82
@ -53,6 +53,7 @@
|
||||
"mkdirp": "^1.0.4",
|
||||
"prom-client": "^12.0.0",
|
||||
"query-string": "^6.13.3",
|
||||
"redis": "^3.1.2",
|
||||
"systeminformation": "^4.31.1",
|
||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||
"uuidv4": "^6.0.7"
|
||||
@ -66,6 +67,7 @@
|
||||
"@types/jasmine": "^3.5.10",
|
||||
"@types/jsonwebtoken": "^8.3.8",
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/redis": "^2.8.31",
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^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 TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
||||
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 {
|
||||
MINIMUM_DISTANCE,
|
||||
|
@ -11,7 +11,8 @@ import {
|
||||
EmoteEventMessage,
|
||||
JoinRoomMessage,
|
||||
SubToPusherRoomMessage,
|
||||
VariableMessage, VariableWithTagMessage,
|
||||
VariableMessage,
|
||||
VariableWithTagMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||
@ -71,7 +72,18 @@ export class GameRoom {
|
||||
): Promise<GameRoom> {
|
||||
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;
|
||||
}
|
||||
@ -381,7 +393,7 @@ export class GameRoom {
|
||||
|
||||
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname);
|
||||
if (!match) {
|
||||
console.error('Unexpected room URL', roomUrl);
|
||||
console.error("Unexpected room URL", roomUrl);
|
||||
throw new Error('Unexpected room URL "' + roomUrl + '"');
|
||||
}
|
||||
|
||||
@ -392,13 +404,13 @@ export class GameRoom {
|
||||
policy_type: 1,
|
||||
textures: [],
|
||||
tags: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const result = await adminApi.fetchMapDetails(roomUrl);
|
||||
if (!isMapDetailsData(result)) {
|
||||
console.error('Unexpected room details received from server', result);
|
||||
throw new Error('Unexpected room details received from server');
|
||||
console.error("Unexpected room details received from server", result);
|
||||
throw new Error("Unexpected room details received from server");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -423,9 +435,19 @@ export class GameRoom {
|
||||
private getVariableManager(): Promise<VariablesManager> {
|
||||
if (!this.variableManagerPromise) {
|
||||
this.variableManagerPromise = new Promise<VariablesManager>((resolve, reject) => {
|
||||
this.getMap().then((map) => {
|
||||
resolve(new VariablesManager(map));
|
||||
}).catch(e => {
|
||||
this.getMap()
|
||||
.then((map) => {
|
||||
const variablesManager = new VariablesManager(this.roomUrl, map);
|
||||
variablesManager
|
||||
.init()
|
||||
.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.
|
||||
@ -434,11 +456,19 @@ export class GameRoom {
|
||||
// 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);
|
||||
variablesManager
|
||||
.init()
|
||||
.then(() => {
|
||||
resolve(variablesManager);
|
||||
})
|
||||
.catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
} else {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
return this.variableManagerPromise;
|
||||
|
@ -4,7 +4,9 @@ import {
|
||||
AdminMessage,
|
||||
AdminPusherToBackMessage,
|
||||
AdminRoomMessage,
|
||||
BanMessage, BatchToPusherMessage, BatchToPusherRoomMessage,
|
||||
BanMessage,
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmotePromptMessage,
|
||||
EmptyMessage,
|
||||
ItemEventMessage,
|
||||
@ -12,10 +14,12 @@ import {
|
||||
PlayGlobalMessage,
|
||||
PusherToBackMessage,
|
||||
QueryJitsiJwtMessage,
|
||||
RefreshRoomPromptMessage, RoomMessage,
|
||||
RefreshRoomPromptMessage,
|
||||
RoomMessage,
|
||||
ServerToAdminClientMessage,
|
||||
SilentMessage,
|
||||
UserMovesMessage, VariableMessage,
|
||||
UserMovesMessage,
|
||||
VariableMessage,
|
||||
WebRtcSignalToServerMessage,
|
||||
WorldFullWarningToRoomMessage,
|
||||
ZoneMessage,
|
||||
@ -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.
|
||||
socketManager.leaveRoom(gameRoom, myUser);
|
||||
}
|
||||
}).catch(e => emitError(call, e));
|
||||
})
|
||||
.catch((e) => emitError(call, e));
|
||||
} else {
|
||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||
}
|
||||
@ -138,22 +143,30 @@ const roomManager: IRoomManagerServer = {
|
||||
debug("listenZone called");
|
||||
const zoneMessage = call.request;
|
||||
|
||||
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()).catch(e => {
|
||||
socketManager
|
||||
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||
.catch((e) => {
|
||||
emitErrorOnZoneSocket(call, e.toString());
|
||||
});
|
||||
|
||||
call.on("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.on("close", () => {
|
||||
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) => {
|
||||
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();
|
||||
});
|
||||
},
|
||||
@ -162,25 +175,24 @@ const roomManager: IRoomManagerServer = {
|
||||
debug("listenRoom called");
|
||||
const roomMessage = call.request;
|
||||
|
||||
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch(e => {
|
||||
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
|
||||
emitErrorOnRoomSocket(call, e.toString());
|
||||
});
|
||||
|
||||
call.on("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.on("close", () => {
|
||||
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) => {
|
||||
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();
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
adminRoom(call: AdminSocket): void {
|
||||
@ -194,9 +206,12 @@ const roomManager: IRoomManagerServer = {
|
||||
if (room === null) {
|
||||
if (message.hasSubscribetoroom()) {
|
||||
const roomId = message.getSubscribetoroom();
|
||||
socketManager.handleJoinAdminRoom(admin, roomId).then((gameRoom: GameRoom) => {
|
||||
socketManager
|
||||
.handleJoinAdminRoom(admin, roomId)
|
||||
.then((gameRoom: GameRoom) => {
|
||||
room = gameRoom;
|
||||
}).catch(e => console.error(e));
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
} else {
|
||||
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 {
|
||||
socketManager.sendAdminMessage(
|
||||
call.request.getRoomid(),
|
||||
call.request.getRecipientuuid(),
|
||||
call.request.getMessage()
|
||||
).catch(e => console.error(e));
|
||||
socketManager
|
||||
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
@ -236,13 +249,17 @@ const roomManager: IRoomManagerServer = {
|
||||
},
|
||||
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||
// 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());
|
||||
},
|
||||
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||
// 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());
|
||||
},
|
||||
sendWorldFullWarningToRoom(
|
||||
@ -250,7 +267,7 @@ const roomManager: IRoomManagerServer = {
|
||||
callback: sendUnaryData<EmptyMessage>
|
||||
): void {
|
||||
// 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());
|
||||
},
|
||||
sendRefreshRoomPrompt(
|
||||
@ -258,9 +275,9 @@ const roomManager: IRoomManagerServer = {
|
||||
callback: sendUnaryData<EmptyMessage>
|
||||
): void {
|
||||
// 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());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export { roomManager };
|
||||
|
@ -1,2 +1 @@
|
||||
export class LocalUrlError extends Error {
|
||||
}
|
||||
export class LocalUrlError extends Error {}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Axios from "axios";
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import { Resolver } from 'dns';
|
||||
import { promisify } from 'util';
|
||||
import ipaddr from "ipaddr.js";
|
||||
import { Resolver } from "dns";
|
||||
import { promisify } from "util";
|
||||
import { LocalUrlError } from "./LocalUrlError";
|
||||
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
|
||||
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||
@ -27,7 +27,7 @@ class MapFetcher {
|
||||
});
|
||||
|
||||
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;
|
||||
@ -39,7 +39,7 @@ class MapFetcher {
|
||||
*/
|
||||
private async isLocalUrl(url: string): Promise<boolean> {
|
||||
const urlObj = new URL(url);
|
||||
if (urlObj.hostname === 'localhost' || urlObj.hostname.endsWith('.localhost')) {
|
||||
if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ class MapFetcher {
|
||||
|
||||
for (const address of addresses) {
|
||||
const addr = ipaddr.parse(address);
|
||||
if (addr.range() !== 'unicast') {
|
||||
if (addr.range() !== "unicast") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import {
|
||||
BatchMessage,
|
||||
BatchToPusherMessage, BatchToPusherRoomMessage,
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
ErrorMessage,
|
||||
ServerToClientMessage, SubToPusherMessage, SubToPusherRoomMessage
|
||||
ServerToClientMessage,
|
||||
SubToPusherMessage,
|
||||
SubToPusherRoomMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { UserSocket } from "_Model/User";
|
||||
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||
|
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,10 +305,12 @@ export class SocketManager {
|
||||
this.onClientLeave(thing, newZone, listener),
|
||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||
this.onEmote(emoteEventMessage, listener)
|
||||
).then((gameRoom) => {
|
||||
)
|
||||
.then((gameRoom) => {
|
||||
gaugeManager.incNbRoomGauge();
|
||||
resolve(gameRoom);
|
||||
}).catch((e) => {
|
||||
})
|
||||
.catch((e) => {
|
||||
this.roomsPromises.delete(roomId);
|
||||
reject(e);
|
||||
});
|
||||
|
@ -3,12 +3,14 @@
|
||||
*/
|
||||
import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist";
|
||||
import { User } from "_Model/User";
|
||||
import { variablesRepository } from "./Repository/VariablesRepository";
|
||||
import { redisClient } from "./RedisClient";
|
||||
|
||||
interface Variable {
|
||||
defaultValue?: string,
|
||||
persist?: boolean,
|
||||
readableBy?: string,
|
||||
writableBy?: string,
|
||||
defaultValue?: string;
|
||||
persist?: boolean;
|
||||
readableBy?: string;
|
||||
writableBy?: string;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
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
|
||||
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
||||
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> {
|
||||
const objects = new Map<string, Variable>();
|
||||
for (const layer of map.layers) {
|
||||
if (layer.type === 'objectgroup') {
|
||||
if (layer.type === "objectgroup") {
|
||||
for (const object of (layer as ITiledMapObjectLayer).objects) {
|
||||
if (object.type === 'variable') {
|
||||
if (object.type === "variable") {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -67,26 +98,30 @@ export class VariablesManager {
|
||||
for (const property of object.properties) {
|
||||
const value = property.value;
|
||||
switch (property.name) {
|
||||
case 'default':
|
||||
case "default":
|
||||
variable.defaultValue = JSON.stringify(value);
|
||||
break;
|
||||
case 'persist':
|
||||
if (typeof value !== 'boolean') {
|
||||
case "persist":
|
||||
if (typeof value !== "boolean") {
|
||||
throw new Error('The persist property of variable "' + object.name + '" must be a boolean');
|
||||
}
|
||||
variable.persist = value;
|
||||
break;
|
||||
case 'writableBy':
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('The writableBy property of variable "' + object.name + '" must be a string');
|
||||
case "writableBy":
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(
|
||||
'The writableBy property of variable "' + object.name + '" must be a string'
|
||||
);
|
||||
}
|
||||
if (value) {
|
||||
variable.writableBy = value;
|
||||
}
|
||||
break;
|
||||
case 'readableBy':
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('The readableBy property of variable "' + object.name + '" must be a string');
|
||||
case "readableBy":
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(
|
||||
'The readableBy property of variable "' + object.name + '" must be a string'
|
||||
);
|
||||
}
|
||||
if (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.');
|
||||
}
|
||||
|
||||
if (variableObject.writableBy && user.tags.indexOf(variableObject.writableBy) === -1) {
|
||||
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(', ') + ".");
|
||||
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(", ") +
|
||||
"."
|
||||
);
|
||||
}
|
||||
|
||||
readableBy = variableObject.readableBy;
|
||||
}
|
||||
|
||||
this._variables.set(name, value);
|
||||
variablesRepository
|
||||
.saveVariable(this.roomUrl, name, value)
|
||||
.catch((e) => console.error("Error while saving variable in Redis:", e));
|
||||
return readableBy;
|
||||
}
|
||||
|
||||
@ -130,7 +178,7 @@ export class VariablesManager {
|
||||
if (variableObject === undefined) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMess
|
||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
|
||||
|
||||
describe("GameRoom", () => {
|
||||
it("should connect user1 and user2", () => {
|
||||
it("should connect user1 and user2", async () => {
|
||||
let connectCalledNumber: number = 0;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
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));
|
||||
@ -67,7 +66,7 @@ describe("GameRoom", () => {
|
||||
expect(connectCalledNumber).toBe(2);
|
||||
});
|
||||
|
||||
it("should connect 3 users", () => {
|
||||
it("should connect 3 users", async () => {
|
||||
let connectCalled: boolean = false;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
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));
|
||||
|
||||
@ -95,7 +94,7 @@ describe("GameRoom", () => {
|
||||
expect(connectCalled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disconnect user1 and user2", () => {
|
||||
it("should disconnect user1 and user2", async () => {
|
||||
let connectCalled: boolean = false;
|
||||
let disconnectCallNumber: number = 0;
|
||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||
@ -105,7 +104,7 @@ describe("GameRoom", () => {
|
||||
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));
|
||||
|
||||
|
@ -122,6 +122,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
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":
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.3"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
|
||||
|
@ -22,6 +22,7 @@
|
||||
"JITSI_URL": env.JITSI_URL,
|
||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||
"REDIS_HOST": "redis",
|
||||
} + (if adminUrl != null then {
|
||||
"ADMIN_API_URL": adminUrl,
|
||||
} else {})
|
||||
@ -40,6 +41,7 @@
|
||||
"JITSI_URL": env.JITSI_URL,
|
||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||
"REDIS_HOST": "redis",
|
||||
} + (if adminUrl != null then {
|
||||
"ADMIN_API_URL": adminUrl,
|
||||
} else {})
|
||||
@ -97,6 +99,9 @@
|
||||
},
|
||||
"ports": [80]
|
||||
},
|
||||
"redis": {
|
||||
"image": "redis:6",
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
k8sextension(k8sConf)::
|
||||
|
@ -120,6 +120,8 @@ services:
|
||||
JITSI_URL: $JITSI_URL
|
||||
JITSI_ISS: $JITSI_ISS
|
||||
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
||||
REDIS_HOST: redis
|
||||
NODE_ENV: development
|
||||
volumes:
|
||||
- ./back:/usr/src/app
|
||||
labels:
|
||||
@ -168,6 +170,9 @@ services:
|
||||
- ./front:/usr/src/front
|
||||
- ./pusher:/usr/src/pusher
|
||||
|
||||
redis:
|
||||
image: redis:6
|
||||
|
||||
# coturn:
|
||||
# image: coturn/coturn:4.5.2
|
||||
# command:
|
||||
|
@ -115,6 +115,8 @@ services:
|
||||
JITSI_ISS: $JITSI_ISS
|
||||
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
||||
MAX_PER_GROUP: "MAX_PER_GROUP"
|
||||
REDIS_HOST: redis
|
||||
NODE_ENV: development
|
||||
volumes:
|
||||
- ./back:/usr/src/app
|
||||
labels:
|
||||
@ -157,6 +159,20 @@ services:
|
||||
- ./front:/usr/src/front
|
||||
- ./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:
|
||||
# image: coturn/coturn:4.5.2
|
||||
# command:
|
||||
|
@ -82,6 +82,8 @@ The object **type** MUST be **variable**.
|
||||
|
||||
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
|
||||
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).
|
||||
@ -89,11 +91,13 @@ server restarts).
|
||||
{.alert.alert-info}
|
||||
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
|
||||
representing a "tag". Anyone having this "tag" can read/write in the variable.
|
||||
|
||||
{.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).
|
||||
|
||||
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 {
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmoteEventMessage, ErrorMessage,
|
||||
EmoteEventMessage,
|
||||
ErrorMessage,
|
||||
GroupLeftZoneMessage,
|
||||
GroupUpdateZoneMessage,
|
||||
RoomMessage,
|
||||
@ -15,7 +16,8 @@ import {
|
||||
UserJoinedZoneMessage,
|
||||
UserLeftZoneMessage,
|
||||
UserMovedMessage,
|
||||
VariableMessage, VariableWithTagMessage,
|
||||
VariableMessage,
|
||||
VariableWithTagMessage,
|
||||
ZoneMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import Debug from "debug";
|
||||
@ -99,7 +101,7 @@ export class PusherRoom {
|
||||
// Let's dispatch this variable to all the listeners
|
||||
for (const listener of this.listeners) {
|
||||
const subMessage = new SubMessage();
|
||||
if (!readableBy || listener.tags.indexOf(readableBy) !== -1) {
|
||||
if (!readableBy || listener.tags.includes(readableBy)) {
|
||||
subMessage.setVariablemessage(variableMessage);
|
||||
}
|
||||
listener.emitInBatch(subMessage);
|
||||
|
@ -14,7 +14,8 @@ import {
|
||||
UserMovedMessage,
|
||||
ZoneMessage,
|
||||
EmoteEventMessage,
|
||||
CompanionMessage, ErrorMessage,
|
||||
CompanionMessage,
|
||||
ErrorMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { ClientReadableStream } from "grpc";
|
||||
import { PositionDispatcher } from "_Model/PositionDispatcher";
|
||||
|
@ -30,7 +30,8 @@ import {
|
||||
BanMessage,
|
||||
RefreshRoomMessage,
|
||||
EmotePromptMessage,
|
||||
VariableMessage, ErrorMessage,
|
||||
VariableMessage,
|
||||
ErrorMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
import { ADMIN_API_URL, JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
||||
|
Loading…
Reference in New Issue
Block a user