Adding support for "readableBy" and "writableBy" in back
This means that we are now loading maps from server side.
This commit is contained in:
parent
3d76f76d3e
commit
dbd5b80636
24 changed files with 768 additions and 132 deletions
24
back/src/Services/AdminApi.ts
Normal file
24
back/src/Services/AdminApi.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import Axios from "axios";
|
||||
import { MapDetailsData } from "./AdminApi/MapDetailsData";
|
||||
import { RoomRedirect } from "./AdminApi/RoomRedirect";
|
||||
|
||||
class AdminApi {
|
||||
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
||||
if (!ADMIN_API_URL) {
|
||||
return Promise.reject(new Error("No admin backoffice set!"));
|
||||
}
|
||||
|
||||
const params: { playUri: string } = {
|
||||
playUri,
|
||||
};
|
||||
|
||||
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||
params,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const adminApi = new AdminApi();
|
11
back/src/Services/AdminApi/CharacterTexture.ts
Normal file
11
back/src/Services/AdminApi/CharacterTexture.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isCharacterTexture = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isNumber,
|
||||
level: tg.isNumber,
|
||||
url: tg.isString,
|
||||
rights: tg.isString,
|
||||
})
|
||||
.get();
|
||||
export type CharacterTexture = tg.GuardedType<typeof isCharacterTexture>;
|
21
back/src/Services/AdminApi/MapDetailsData.ts
Normal file
21
back/src/Services/AdminApi/MapDetailsData.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
import { isCharacterTexture } from "./CharacterTexture";
|
||||
import { isAny, isNumber } from "generic-type-guard";
|
||||
|
||||
/*const isNumericEnum =
|
||||
<T extends { [n: number]: string }>(vs: T) =>
|
||||
(v: any): v is T =>
|
||||
typeof v === "number" && v in vs;*/
|
||||
|
||||
export const isMapDetailsData = new tg.IsInterface()
|
||||
.withProperties({
|
||||
mapUrl: tg.isString,
|
||||
policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes),
|
||||
tags: tg.isArray(tg.isString),
|
||||
textures: tg.isArray(isCharacterTexture),
|
||||
})
|
||||
.withOptionalProperties({
|
||||
roomSlug: tg.isUnion(tg.isString, tg.isNull), // deprecated
|
||||
})
|
||||
.get();
|
||||
export type MapDetailsData = tg.GuardedType<typeof isMapDetailsData>;
|
8
back/src/Services/AdminApi/RoomRedirect.ts
Normal file
8
back/src/Services/AdminApi/RoomRedirect.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isRoomRedirect = new tg.IsInterface()
|
||||
.withProperties({
|
||||
redirectUrl: tg.isString,
|
||||
})
|
||||
.get();
|
||||
export type RoomRedirect = tg.GuardedType<typeof isRoomRedirect>;
|
2
back/src/Services/LocalUrlError.ts
Normal file
2
back/src/Services/LocalUrlError.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export class LocalUrlError extends Error {
|
||||
}
|
64
back/src/Services/MapFetcher.ts
Normal file
64
back/src/Services/MapFetcher.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import Axios from "axios";
|
||||
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";
|
||||
|
||||
class MapFetcher {
|
||||
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)
|
||||
|
||||
if (await this.isLocalUrl(mapUrl)) {
|
||||
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
|
||||
// returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially
|
||||
// target to different servers (and one could trick Axios.get into loading resources on the internal network
|
||||
// despite isLocalUrl checking that.
|
||||
// We can deem this problem not that important because:
|
||||
// - We make sure we are only passing "GET" requests
|
||||
// - The result of the query is never displayed to the end user
|
||||
const res = await Axios.get(mapUrl, {
|
||||
maxContentLength: 50*1024*1024, // Max content length: 50MB. Maps should not be bigger
|
||||
});
|
||||
|
||||
if (!isTiledMap(res.data)) {
|
||||
throw new Error('Invalid map format for map '+mapUrl);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the domain name is localhost of *.localhost
|
||||
* Returns true if the domain name resolves to an IP address that is "private" (like 10.x.x.x or 192.168.x.x)
|
||||
*/
|
||||
private async isLocalUrl(url: string): Promise<boolean> {
|
||||
const urlObj = new URL(url);
|
||||
if (urlObj.hostname === 'localhost' || urlObj.hostname.endsWith('.localhost')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let addresses = [];
|
||||
if (!ipaddr.isValid(urlObj.hostname)) {
|
||||
const resolver = new Resolver();
|
||||
addresses = await promisify(resolver.resolve)(urlObj.hostname);
|
||||
} else {
|
||||
addresses = [urlObj.hostname];
|
||||
}
|
||||
|
||||
for (const address of addresses) {
|
||||
const addr = ipaddr.parse(address);
|
||||
if (addr.range() !== 'unicast') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const mapFetcher = new MapFetcher();
|
|
@ -1,5 +1,11 @@
|
|||
import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb";
|
||||
import {
|
||||
BatchMessage,
|
||||
BatchToPusherMessage, BatchToPusherRoomMessage,
|
||||
ErrorMessage,
|
||||
ServerToClientMessage, SubToPusherMessage, SubToPusherRoomMessage
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { UserSocket } from "_Model/User";
|
||||
import {RoomSocket, ZoneSocket} from "../RoomManager";
|
||||
|
||||
export function emitError(Client: UserSocket, message: string): void {
|
||||
const errorMessage = new ErrorMessage();
|
||||
|
@ -13,3 +19,39 @@ export function emitError(Client: UserSocket, message: string): void {
|
|||
//}
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void {
|
||||
console.error(message);
|
||||
|
||||
const errorMessage = new ErrorMessage();
|
||||
errorMessage.setMessage(message);
|
||||
|
||||
const subToPusherRoomMessage = new SubToPusherRoomMessage();
|
||||
subToPusherRoomMessage.setErrormessage(errorMessage);
|
||||
|
||||
const batchToPusherMessage = new BatchToPusherRoomMessage();
|
||||
batchToPusherMessage.addPayload(subToPusherRoomMessage);
|
||||
|
||||
//if (!Client.disconnecting) {
|
||||
Client.write(batchToPusherMessage);
|
||||
//}
|
||||
console.warn(message);
|
||||
}
|
||||
|
||||
export function emitErrorOnZoneSocket(Client: ZoneSocket, message: string): void {
|
||||
console.error(message);
|
||||
|
||||
const errorMessage = new ErrorMessage();
|
||||
errorMessage.setMessage(message);
|
||||
|
||||
const subToPusherMessage = new SubToPusherMessage();
|
||||
subToPusherMessage.setErrormessage(errorMessage);
|
||||
|
||||
const batchToPusherMessage = new BatchToPusherMessage();
|
||||
batchToPusherMessage.addPayload(subToPusherMessage);
|
||||
|
||||
//if (!Client.disconnecting) {
|
||||
Client.write(batchToPusherMessage);
|
||||
//}
|
||||
console.warn(message);
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo
|
|||
}
|
||||
|
||||
export class SocketManager {
|
||||
private rooms = new Map<string, GameRoom>();
|
||||
//private rooms = new Map<string, GameRoom>();
|
||||
// List of rooms in process of loading.
|
||||
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
||||
|
||||
|
@ -106,7 +106,9 @@ export class SocketManager {
|
|||
roomJoinedMessage.addItem(itemStateMessage);
|
||||
}
|
||||
|
||||
for (const [name, value] of room.variables.entries()) {
|
||||
const variables = await room.getVariablesForTags(user.tags);
|
||||
|
||||
for (const [name, value] of variables.entries()) {
|
||||
const variableMessage = new VariableMessage();
|
||||
variableMessage.setName(name);
|
||||
variableMessage.setValue(value);
|
||||
|
@ -198,12 +200,14 @@ export class SocketManager {
|
|||
}
|
||||
|
||||
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage) {
|
||||
try {
|
||||
room.setVariable(variableMessage.getName(), variableMessage.getValue());
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "handleVariableEvent"');
|
||||
console.error(e);
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
await room.setVariable(variableMessage.getName(), variableMessage.getValue(), user);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "handleVariableEvent"');
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
||||
|
@ -272,7 +276,7 @@ export class SocketManager {
|
|||
//user leave previous world
|
||||
room.leave(user);
|
||||
if (room.isEmpty()) {
|
||||
this.rooms.delete(room.roomUrl);
|
||||
this.roomsPromises.delete(room.roomUrl);
|
||||
gaugeManager.decNbRoomGauge();
|
||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||
}
|
||||
|
@ -284,38 +288,34 @@ export class SocketManager {
|
|||
|
||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
||||
//check and create new room
|
||||
let room = this.rooms.get(roomId);
|
||||
if (room === undefined) {
|
||||
let roomPromise = this.roomsPromises.get(roomId);
|
||||
if (roomPromise) {
|
||||
return roomPromise;
|
||||
}
|
||||
|
||||
// Note: for now, the promise is useless (because this is synchronous, but soon, we will need to
|
||||
// load the map server side.
|
||||
|
||||
room = new GameRoom(
|
||||
roomId,
|
||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
||||
MINIMUM_DISTANCE,
|
||||
GROUP_RADIUS,
|
||||
(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
|
||||
this.onZoneEnter(thing, fromZone, listener),
|
||||
(thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
|
||||
this.onClientMove(thing, position, listener),
|
||||
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
||||
this.onClientLeave(thing, newZone, listener),
|
||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||
this.onEmote(emoteEventMessage, listener)
|
||||
);
|
||||
gaugeManager.incNbRoomGauge();
|
||||
this.rooms.set(roomId, room);
|
||||
|
||||
// TODO: change this the to new Promise()... when the method becomes actually asynchronous
|
||||
roomPromise = Promise.resolve(room);
|
||||
let roomPromise = this.roomsPromises.get(roomId);
|
||||
if (roomPromise === undefined) {
|
||||
roomPromise = new Promise<GameRoom>((resolve, reject) => {
|
||||
GameRoom.create(
|
||||
roomId,
|
||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
||||
MINIMUM_DISTANCE,
|
||||
GROUP_RADIUS,
|
||||
(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
|
||||
this.onZoneEnter(thing, fromZone, listener),
|
||||
(thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
|
||||
this.onClientMove(thing, position, listener),
|
||||
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
||||
this.onClientLeave(thing, newZone, listener),
|
||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||
this.onEmote(emoteEventMessage, listener)
|
||||
).then((gameRoom) => {
|
||||
gaugeManager.incNbRoomGauge();
|
||||
resolve(gameRoom);
|
||||
}).catch((e) => {
|
||||
this.roomsPromises.delete(roomId);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
this.roomsPromises.set(roomId, roomPromise);
|
||||
}
|
||||
return Promise.resolve(room);
|
||||
return roomPromise;
|
||||
}
|
||||
|
||||
private async joinRoom(
|
||||
|
@ -554,8 +554,8 @@ export class SocketManager {
|
|||
}
|
||||
}
|
||||
|
||||
public getWorlds(): Map<string, GameRoom> {
|
||||
return this.rooms;
|
||||
public getWorlds(): Map<string, PromiseLike<GameRoom>> {
|
||||
return this.roomsPromises;
|
||||
}
|
||||
|
||||
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
||||
|
@ -625,11 +625,10 @@ export class SocketManager {
|
|||
}, 10000);
|
||||
}
|
||||
|
||||
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
public async addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error("In addZoneListener, could not find room with id '" + roomId + "'");
|
||||
return;
|
||||
throw new Error("In addZoneListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
const things = room.addZoneListener(call, x, y);
|
||||
|
@ -670,11 +669,10 @@ export class SocketManager {
|
|||
call.write(batchMessage);
|
||||
}
|
||||
|
||||
removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) {
|
||||
const room = this.rooms.get(roomId);
|
||||
async removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
||||
return;
|
||||
throw new Error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
room.removeZoneListener(call, x, y);
|
||||
|
@ -683,8 +681,7 @@ export class SocketManager {
|
|||
async addRoomListener(call: RoomSocket, roomId: string) {
|
||||
const room = await this.getOrCreateRoom(roomId);
|
||||
if (!room) {
|
||||
console.error("In addRoomListener, could not find room with id '" + roomId + "'");
|
||||
return;
|
||||
throw new Error("In addRoomListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
room.addRoomListener(call);
|
||||
|
@ -692,7 +689,10 @@ export class SocketManager {
|
|||
|
||||
const batchMessage = new BatchToPusherRoomMessage();
|
||||
|
||||
for (const [name, value] of room.variables.entries()) {
|
||||
// Finally, no need to store variables in the pusher, let's only make it act as a relay
|
||||
/*const variables = await room.getVariables();
|
||||
|
||||
for (const [name, value] of variables.entries()) {
|
||||
const variableMessage = new VariableMessage();
|
||||
variableMessage.setName(name);
|
||||
variableMessage.setValue(value);
|
||||
|
@ -701,16 +701,15 @@ export class SocketManager {
|
|||
subMessage.setVariablemessage(variableMessage);
|
||||
|
||||
batchMessage.addPayload(subMessage);
|
||||
}
|
||||
}*/
|
||||
|
||||
call.write(batchMessage);
|
||||
}
|
||||
|
||||
removeRoomListener(call: RoomSocket, roomId: string) {
|
||||
const room = this.rooms.get(roomId);
|
||||
async removeRoomListener(call: RoomSocket, roomId: string) {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
||||
return;
|
||||
throw new Error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
room.removeRoomListener(call);
|
||||
|
@ -727,14 +726,14 @@ export class SocketManager {
|
|||
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||
room.adminLeave(admin);
|
||||
if (room.isEmpty()) {
|
||||
this.rooms.delete(room.roomUrl);
|
||||
this.roomsPromises.delete(room.roomUrl);
|
||||
gaugeManager.decNbRoomGauge();
|
||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error(
|
||||
"In sendAdminMessage, could not find room with id '" +
|
||||
|
@ -764,8 +763,8 @@ export class SocketManager {
|
|||
recipient.socket.write(serverToClientMessage);
|
||||
}
|
||||
|
||||
public banUser(roomId: string, recipientUuid: string, message: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
public async banUser(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
console.error(
|
||||
"In banUser, could not find room with id '" +
|
||||
|
@ -800,8 +799,8 @@ export class SocketManager {
|
|||
recipient.socket.end();
|
||||
}
|
||||
|
||||
sendAdminRoomMessage(roomId: string, message: string) {
|
||||
const room = this.rooms.get(roomId);
|
||||
async sendAdminRoomMessage(roomId: string, message: string) {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
//todo: this should cause the http call to return a 500
|
||||
console.error(
|
||||
|
@ -824,8 +823,8 @@ export class SocketManager {
|
|||
});
|
||||
}
|
||||
|
||||
dispatchWorlFullWarning(roomId: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
async dispatchWorldFullWarning(roomId: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
//todo: this should cause the http call to return a 500
|
||||
console.error(
|
||||
|
@ -846,8 +845,8 @@ export class SocketManager {
|
|||
});
|
||||
}
|
||||
|
||||
dispatchRoomRefresh(roomId: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
async dispatchRoomRefresh(roomId: string): Promise<void> {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
|
139
back/src/Services/VariablesManager.ts
Normal file
139
back/src/Services/VariablesManager.ts
Normal file
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* Handles variables shared between the scripting API and the server.
|
||||
*/
|
||||
import {ITiledMap, ITiledMapObject, ITiledMapObjectLayer} from "@workadventure/tiled-map-type-guard/dist";
|
||||
import {User} from "_Model/User";
|
||||
|
||||
interface Variable {
|
||||
defaultValue?: string,
|
||||
persist?: boolean,
|
||||
readableBy?: string,
|
||||
writableBy?: string,
|
||||
}
|
||||
|
||||
export class VariablesManager {
|
||||
/**
|
||||
* The actual values of the variables for the current room
|
||||
*/
|
||||
private _variables = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* The list of variables that are allowed
|
||||
*/
|
||||
private variableObjects: Map<string, Variable> | undefined;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
// 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) {
|
||||
this.variableObjects = VariablesManager.findVariablesInMap(map);
|
||||
|
||||
// Let's initialize default values
|
||||
for (const [name, variableObject] of this.variableObjects.entries()) {
|
||||
if (variableObject.defaultValue !== undefined) {
|
||||
this._variables.set(name, variableObject.defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
|
||||
const objects = new Map<string, Variable>();
|
||||
for (const layer of map.layers) {
|
||||
if (layer.type === 'objectgroup') {
|
||||
for (const object of (layer as ITiledMapObjectLayer).objects) {
|
||||
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.')
|
||||
continue;
|
||||
}
|
||||
|
||||
// We store a copy of the object (to make it immutable)
|
||||
objects.set(object.name, this.iTiledObjectToVariable(object));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
|
||||
const variable: Variable = {};
|
||||
|
||||
if (object.properties) {
|
||||
for (const property of object.properties) {
|
||||
const value = property.value;
|
||||
switch (property.name) {
|
||||
case 'default':
|
||||
variable.defaultValue = JSON.stringify(value);
|
||||
break;
|
||||
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');
|
||||
}
|
||||
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');
|
||||
}
|
||||
if (value) {
|
||||
variable.readableBy = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return variable;
|
||||
}
|
||||
|
||||
setVariable(name: string, value: string, user: User): string | undefined {
|
||||
let readableBy: string | undefined;
|
||||
if (this.variableObjects) {
|
||||
const variableObject = this.variableObjects.get(name);
|
||||
if (variableObject === undefined) {
|
||||
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(', ') + ".");
|
||||
}
|
||||
|
||||
readableBy = variableObject.readableBy;
|
||||
}
|
||||
|
||||
this._variables.set(name, value);
|
||||
return readableBy;
|
||||
}
|
||||
|
||||
public getVariablesForTags(tags: string[]): Map<string, string> {
|
||||
if (this.variableObjects === undefined) {
|
||||
return this._variables;
|
||||
}
|
||||
|
||||
const readableVariables = new Map<string, string>();
|
||||
|
||||
for (const [key, value] of this._variables.entries()) {
|
||||
const variableObject = this.variableObjects.get(key);
|
||||
if (variableObject === undefined) {
|
||||
throw new Error('Unexpected variable "'+key+'" found has no associated variableObject.');
|
||||
}
|
||||
if (!variableObject.readableBy || tags.indexOf(variableObject.readableBy) !== -1) {
|
||||
readableVariables.set(key, value);
|
||||
}
|
||||
}
|
||||
return readableVariables;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue