Merging with develop
This commit is contained in:
parent
3d5c222957
commit
cdd61bdb2c
135 changed files with 4313 additions and 2128 deletions
|
@ -40,6 +40,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||
"dependencies": {
|
||||
"@workadventure/tiled-map-type-guard": "^1.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"busboy": "^0.3.1",
|
||||
"circular-json": "^0.5.9",
|
||||
|
@ -47,10 +48,12 @@
|
|||
"generic-type-guard": "^3.2.0",
|
||||
"google-protobuf": "^3.13.0",
|
||||
"grpc": "^1.24.4",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"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"
|
||||
|
@ -64,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",
|
||||
|
|
|
@ -15,7 +15,7 @@ export class DebugController {
|
|||
const query = parse(req.getQuery());
|
||||
|
||||
if (query.token !== ADMIN_API_TOKEN) {
|
||||
return res.status(401).send("Invalid token sent!");
|
||||
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
||||
}
|
||||
|
||||
return res
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -5,47 +5,64 @@ import { PositionInterface } from "_Model/PositionInterface";
|
|||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||
import { PositionNotifier } from "./PositionNotifier";
|
||||
import { Movable } from "_Model/Movable";
|
||||
import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
|
||||
import { arrayIntersect } from "../Services/ArrayHelper";
|
||||
import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb";
|
||||
import {
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmoteEventMessage,
|
||||
ErrorMessage,
|
||||
JoinRoomMessage,
|
||||
SubToPusherRoomMessage,
|
||||
VariableMessage,
|
||||
VariableWithTagMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
import { ZoneSocket } from "src/RoomManager";
|
||||
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||
import { Admin } from "../Model/Admin";
|
||||
import { adminApi } from "../Services/AdminApi";
|
||||
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
||||
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||
import { mapFetcher } from "../Services/MapFetcher";
|
||||
import { VariablesManager } from "../Services/VariablesManager";
|
||||
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import { LocalUrlError } from "../Services/LocalUrlError";
|
||||
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
||||
|
||||
export type ConnectCallback = (user: User, group: Group) => void;
|
||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||
|
||||
export enum GameRoomPolicyTypes {
|
||||
ANONYMOUS_POLICY = 1,
|
||||
MEMBERS_ONLY_POLICY,
|
||||
USE_TAGS_POLICY,
|
||||
}
|
||||
|
||||
export class GameRoom {
|
||||
private readonly minDistance: number;
|
||||
private readonly groupRadius: number;
|
||||
|
||||
// Users, sorted by ID
|
||||
private readonly users: Map<number, User>;
|
||||
private readonly usersByUuid: Map<string, User>;
|
||||
private readonly groups: Set<Group>;
|
||||
private readonly admins: Set<Admin>;
|
||||
private readonly users = new Map<number, User>();
|
||||
private readonly usersByUuid = new Map<string, User>();
|
||||
private readonly groups = new Set<Group>();
|
||||
private readonly admins = new Set<Admin>();
|
||||
|
||||
private readonly connectCallback: ConnectCallback;
|
||||
private readonly disconnectCallback: DisconnectCallback;
|
||||
|
||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||
private itemsState = new Map<number, unknown>();
|
||||
|
||||
private readonly positionNotifier: PositionNotifier;
|
||||
public readonly roomId: string;
|
||||
public readonly roomSlug: string;
|
||||
public readonly worldSlug: string = "";
|
||||
public readonly organizationSlug: string = "";
|
||||
private versionNumber: number = 1;
|
||||
private nextUserId: number = 1;
|
||||
|
||||
constructor(
|
||||
roomId: string,
|
||||
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
||||
|
||||
private constructor(
|
||||
public readonly roomUrl: string,
|
||||
private mapUrl: string,
|
||||
private readonly connectCallback: ConnectCallback,
|
||||
private readonly disconnectCallback: DisconnectCallback,
|
||||
private readonly minDistance: number,
|
||||
private readonly groupRadius: number,
|
||||
onEnters: EntersCallback,
|
||||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
) {
|
||||
// A zone is 10 sprites wide.
|
||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
||||
}
|
||||
|
||||
public static async create(
|
||||
roomUrl: string,
|
||||
connectCallback: ConnectCallback,
|
||||
disconnectCallback: DisconnectCallback,
|
||||
minDistance: number,
|
||||
|
@ -54,28 +71,23 @@ export class GameRoom {
|
|||
onMoves: MovesCallback,
|
||||
onLeaves: LeavesCallback,
|
||||
onEmote: EmoteCallback
|
||||
) {
|
||||
this.roomId = roomId;
|
||||
): Promise<GameRoom> {
|
||||
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
||||
|
||||
if (isRoomAnonymous(roomId)) {
|
||||
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
||||
} else {
|
||||
const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
|
||||
this.roomSlug = roomSlug;
|
||||
this.organizationSlug = organizationSlug;
|
||||
this.worldSlug = worldSlug;
|
||||
}
|
||||
const gameRoom = new GameRoom(
|
||||
roomUrl,
|
||||
mapDetails.mapUrl,
|
||||
connectCallback,
|
||||
disconnectCallback,
|
||||
minDistance,
|
||||
groupRadius,
|
||||
onEnters,
|
||||
onMoves,
|
||||
onLeaves,
|
||||
onEmote
|
||||
);
|
||||
|
||||
this.users = new Map<number, User>();
|
||||
this.usersByUuid = new Map<string, User>();
|
||||
this.admins = new Set<Admin>();
|
||||
this.groups = new Set<Group>();
|
||||
this.connectCallback = connectCallback;
|
||||
this.disconnectCallback = disconnectCallback;
|
||||
this.minDistance = minDistance;
|
||||
this.groupRadius = groupRadius;
|
||||
// A zone is 10 sprites wide.
|
||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
||||
return gameRoom;
|
||||
}
|
||||
|
||||
public getGroups(): Group[] {
|
||||
|
@ -183,7 +195,7 @@ export class GameRoom {
|
|||
} else {
|
||||
const closestUser: User = closestItem;
|
||||
const group: Group = new Group(
|
||||
this.roomId,
|
||||
this.roomUrl,
|
||||
[user, closestUser],
|
||||
this.connectCallback,
|
||||
this.disconnectCallback,
|
||||
|
@ -309,6 +321,37 @@ export class GameRoom {
|
|||
return this.itemsState;
|
||||
}
|
||||
|
||||
public async setVariable(name: string, value: string, user: User): Promise<void> {
|
||||
// First, let's check if "user" is allowed to modify the variable.
|
||||
const variableManager = await this.getVariableManager();
|
||||
|
||||
const readableBy = variableManager.setVariable(name, value, user);
|
||||
|
||||
// If the variable was not changed, let's not dispatch anything.
|
||||
if (readableBy === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: should we batch those every 100ms?
|
||||
const variableMessage = new VariableWithTagMessage();
|
||||
variableMessage.setName(name);
|
||||
variableMessage.setValue(value);
|
||||
if (readableBy) {
|
||||
variableMessage.setReadableby(readableBy);
|
||||
}
|
||||
|
||||
const subMessage = new SubToPusherRoomMessage();
|
||||
subMessage.setVariablemessage(variableMessage);
|
||||
|
||||
const batchMessage = new BatchToPusherRoomMessage();
|
||||
batchMessage.addPayload(subMessage);
|
||||
|
||||
// Dispatch the message on the room listeners
|
||||
for (const socket of this.roomListeners) {
|
||||
socket.write(batchMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
||||
return this.positionNotifier.addZoneListener(call, x, y);
|
||||
}
|
||||
|
@ -338,4 +381,98 @@ export class GameRoom {
|
|||
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
||||
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
|
||||
}
|
||||
|
||||
public addRoomListener(socket: RoomSocket) {
|
||||
this.roomListeners.add(socket);
|
||||
}
|
||||
|
||||
public removeRoomListener(socket: RoomSocket) {
|
||||
this.roomListeners.delete(socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the admin server to fetch map details.
|
||||
* If there is no admin server, the map details are generated by analysing the map URL (that must be in the form: /_/instance/map_url)
|
||||
*/
|
||||
private static async getMapDetails(roomUrl: string): Promise<MapDetailsData> {
|
||||
if (!ADMIN_API_URL) {
|
||||
const roomUrlObj = new URL(roomUrl);
|
||||
|
||||
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname);
|
||||
if (!match) {
|
||||
console.error("Unexpected room URL", roomUrl);
|
||||
throw new Error('Unexpected room URL "' + roomUrl + '"');
|
||||
}
|
||||
|
||||
const mapUrl = roomUrlObj.protocol + "//" + match[1];
|
||||
|
||||
return {
|
||||
mapUrl,
|
||||
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");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private mapPromise: Promise<ITiledMap> | undefined;
|
||||
|
||||
/**
|
||||
* Returns a promise to the map file.
|
||||
* @throws LocalUrlError if the map we are trying to load is hosted on a local network
|
||||
* @throws Error
|
||||
*/
|
||||
private getMap(): Promise<ITiledMap> {
|
||||
if (!this.mapPromise) {
|
||||
this.mapPromise = mapFetcher.fetchMap(this.mapUrl);
|
||||
}
|
||||
|
||||
return this.mapPromise;
|
||||
}
|
||||
|
||||
private variableManagerPromise: Promise<VariablesManager> | undefined;
|
||||
|
||||
private getVariableManager(): Promise<VariablesManager> {
|
||||
if (!this.variableManagerPromise) {
|
||||
this.variableManagerPromise = this.getMap()
|
||||
.then((map) => {
|
||||
const variablesManager = new VariablesManager(this.roomUrl, map);
|
||||
return variablesManager.init();
|
||||
})
|
||||
.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.
|
||||
|
||||
// Note: we run this message inside a setTimeout so that the room listeners can have time to connect.
|
||||
setTimeout(() => {
|
||||
for (const roomListener of this.roomListeners) {
|
||||
emitErrorOnRoomSocket(
|
||||
roomListener,
|
||||
"You are loading a local map. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled."
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const variablesManager = new VariablesManager(this.roomUrl, null);
|
||||
return variablesManager.init();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.variableManagerPromise;
|
||||
}
|
||||
|
||||
public async getVariablesForTags(tags: string[]): Promise<Map<string, string>> {
|
||||
const variablesManager = await this.getVariableManager();
|
||||
return variablesManager.getVariablesForTags(tags);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
//helper functions to parse room IDs
|
||||
|
||||
export const isRoomAnonymous = (roomID: string): boolean => {
|
||||
if (roomID.startsWith("_/")) {
|
||||
return true;
|
||||
} else if (roomID.startsWith("@/")) {
|
||||
return false;
|
||||
} else {
|
||||
throw new Error("Incorrect room ID: " + roomID);
|
||||
}
|
||||
};
|
||||
|
||||
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
||||
const idParts = roomId.split("/");
|
||||
if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId);
|
||||
return idParts.slice(2).join("/");
|
||||
};
|
||||
export interface extractDataFromPrivateRoomIdResponse {
|
||||
organizationSlug: string;
|
||||
worldSlug: string;
|
||||
roomSlug: string;
|
||||
}
|
||||
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
||||
const idParts = roomId.split("/");
|
||||
if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId);
|
||||
const organizationSlug = idParts[1];
|
||||
const worldSlug = idParts[2];
|
||||
const roomSlug = idParts[3];
|
||||
return { organizationSlug, worldSlug, roomSlug };
|
||||
};
|
|
@ -5,6 +5,8 @@ import {
|
|||
AdminPusherToBackMessage,
|
||||
AdminRoomMessage,
|
||||
BanMessage,
|
||||
BatchToPusherMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
EmotePromptMessage,
|
||||
EmptyMessage,
|
||||
ItemEventMessage,
|
||||
|
@ -13,17 +15,18 @@ import {
|
|||
PusherToBackMessage,
|
||||
QueryJitsiJwtMessage,
|
||||
RefreshRoomPromptMessage,
|
||||
RoomMessage,
|
||||
ServerToAdminClientMessage,
|
||||
ServerToClientMessage,
|
||||
SilentMessage,
|
||||
UserMovesMessage,
|
||||
VariableMessage,
|
||||
WebRtcSignalToServerMessage,
|
||||
WorldFullWarningToRoomMessage,
|
||||
ZoneMessage,
|
||||
} from "./Messages/generated/messages_pb";
|
||||
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
||||
import { socketManager } from "./Services/SocketManager";
|
||||
import { emitError } from "./Services/MessageHelpers";
|
||||
import { emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket } from "./Services/MessageHelpers";
|
||||
import { User, UserSocket } from "./Model/User";
|
||||
import { GameRoom } from "./Model/GameRoom";
|
||||
import Debug from "debug";
|
||||
|
@ -32,7 +35,8 @@ import { Admin } from "./Model/Admin";
|
|||
const debug = Debug("roommanager");
|
||||
|
||||
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
|
||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, BatchToPusherMessage>;
|
||||
export type RoomSocket = ServerWritableStream<RoomMessage, BatchToPusherRoomMessage>;
|
||||
|
||||
const roomManager: IRoomManagerServer = {
|
||||
joinRoom: (call: UserSocket): void => {
|
||||
|
@ -42,79 +46,96 @@ const roomManager: IRoomManagerServer = {
|
|||
let user: User | null = null;
|
||||
|
||||
call.on("data", (message: PusherToBackMessage) => {
|
||||
try {
|
||||
if (room === null || user === null) {
|
||||
if (message.hasJoinroommessage()) {
|
||||
socketManager
|
||||
.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
|
||||
.then(({ room: gameRoom, user: myUser }) => {
|
||||
if (call.writable) {
|
||||
room = gameRoom;
|
||||
user = myUser;
|
||||
} else {
|
||||
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
||||
socketManager.leaveRoom(gameRoom, myUser);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||
}
|
||||
} else {
|
||||
if (message.hasJoinroommessage()) {
|
||||
throw new Error("Cannot call JoinRoomMessage twice!");
|
||||
} else if (message.hasUsermovesmessage()) {
|
||||
socketManager.handleUserMovesMessage(
|
||||
room,
|
||||
user,
|
||||
message.getUsermovesmessage() as UserMovesMessage
|
||||
);
|
||||
} else if (message.hasSilentmessage()) {
|
||||
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
||||
} else if (message.hasItemeventmessage()) {
|
||||
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
|
||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||
socketManager.emitVideo(
|
||||
room,
|
||||
user,
|
||||
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||
);
|
||||
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||
socketManager.emitScreenSharing(
|
||||
room,
|
||||
user,
|
||||
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||
);
|
||||
} else if (message.hasPlayglobalmessage()) {
|
||||
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
|
||||
} else if (message.hasQueryjitsijwtmessage()) {
|
||||
socketManager.handleQueryJitsiJwtMessage(
|
||||
user,
|
||||
message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
|
||||
);
|
||||
} else if (message.hasEmotepromptmessage()) {
|
||||
socketManager.handleEmoteEventMessage(
|
||||
room,
|
||||
user,
|
||||
message.getEmotepromptmessage() as EmotePromptMessage
|
||||
);
|
||||
} else if (message.hasSendusermessage()) {
|
||||
const sendUserMessage = message.getSendusermessage();
|
||||
if (sendUserMessage !== undefined) {
|
||||
socketManager.handlerSendUserMessage(user, sendUserMessage);
|
||||
}
|
||||
} else if (message.hasBanusermessage()) {
|
||||
const banUserMessage = message.getBanusermessage();
|
||||
if (banUserMessage !== undefined) {
|
||||
socketManager.handlerBanUserMessage(room, user, banUserMessage);
|
||||
(async () => {
|
||||
try {
|
||||
if (room === null || user === null) {
|
||||
if (message.hasJoinroommessage()) {
|
||||
socketManager
|
||||
.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
|
||||
.then(({ room: gameRoom, user: myUser }) => {
|
||||
if (call.writable) {
|
||||
room = gameRoom;
|
||||
user = myUser;
|
||||
} else {
|
||||
//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));
|
||||
} else {
|
||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unhandled message type");
|
||||
if (message.hasJoinroommessage()) {
|
||||
throw new Error("Cannot call JoinRoomMessage twice!");
|
||||
} else if (message.hasUsermovesmessage()) {
|
||||
socketManager.handleUserMovesMessage(
|
||||
room,
|
||||
user,
|
||||
message.getUsermovesmessage() as UserMovesMessage
|
||||
);
|
||||
} else if (message.hasSilentmessage()) {
|
||||
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
||||
} else if (message.hasItemeventmessage()) {
|
||||
socketManager.handleItemEvent(
|
||||
room,
|
||||
user,
|
||||
message.getItemeventmessage() as ItemEventMessage
|
||||
);
|
||||
} else if (message.hasVariablemessage()) {
|
||||
await socketManager.handleVariableEvent(
|
||||
room,
|
||||
user,
|
||||
message.getVariablemessage() as VariableMessage
|
||||
);
|
||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||
socketManager.emitVideo(
|
||||
room,
|
||||
user,
|
||||
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||
);
|
||||
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||
socketManager.emitScreenSharing(
|
||||
room,
|
||||
user,
|
||||
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||
);
|
||||
} else if (message.hasPlayglobalmessage()) {
|
||||
socketManager.emitPlayGlobalMessage(
|
||||
room,
|
||||
message.getPlayglobalmessage() as PlayGlobalMessage
|
||||
);
|
||||
} else if (message.hasQueryjitsijwtmessage()) {
|
||||
socketManager.handleQueryJitsiJwtMessage(
|
||||
user,
|
||||
message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
|
||||
);
|
||||
} else if (message.hasEmotepromptmessage()) {
|
||||
socketManager.handleEmoteEventMessage(
|
||||
room,
|
||||
user,
|
||||
message.getEmotepromptmessage() as EmotePromptMessage
|
||||
);
|
||||
} else if (message.hasSendusermessage()) {
|
||||
const sendUserMessage = message.getSendusermessage();
|
||||
if (sendUserMessage !== undefined) {
|
||||
socketManager.handlerSendUserMessage(user, sendUserMessage);
|
||||
}
|
||||
} else if (message.hasBanusermessage()) {
|
||||
const banUserMessage = message.getBanusermessage();
|
||||
if (banUserMessage !== undefined) {
|
||||
socketManager.handlerBanUserMessage(room, user, banUserMessage);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unhandled message type");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
emitError(call, e);
|
||||
call.end();
|
||||
}
|
||||
} catch (e) {
|
||||
emitError(call, e);
|
||||
call.end();
|
||||
}
|
||||
})().catch((e) => console.error(e));
|
||||
});
|
||||
|
||||
call.on("end", () => {
|
||||
|
@ -136,20 +157,54 @@ const roomManager: IRoomManagerServer = {
|
|||
debug("listenZone called");
|
||||
const zoneMessage = call.request;
|
||||
|
||||
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
||||
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());
|
||||
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());
|
||||
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());
|
||||
socketManager
|
||||
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||
.catch((e) => console.error(e));
|
||||
call.end();
|
||||
});
|
||||
},
|
||||
|
||||
listenRoom(call: RoomSocket): void {
|
||||
debug("listenRoom called");
|
||||
const roomMessage = call.request;
|
||||
|
||||
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));
|
||||
call.end();
|
||||
});
|
||||
|
||||
call.on("close", () => {
|
||||
debug("listenRoom connection closed");
|
||||
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));
|
||||
call.end();
|
||||
});
|
||||
},
|
||||
|
@ -165,9 +220,12 @@ const roomManager: IRoomManagerServer = {
|
|||
if (room === null) {
|
||||
if (message.hasSubscribetoroom()) {
|
||||
const roomId = message.getSubscribetoroom();
|
||||
socketManager.handleJoinAdminRoom(admin, roomId).then((gameRoom: GameRoom) => {
|
||||
room = gameRoom;
|
||||
});
|
||||
socketManager
|
||||
.handleJoinAdminRoom(admin, roomId)
|
||||
.then((gameRoom: GameRoom) => {
|
||||
room = gameRoom;
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
} else {
|
||||
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||
}
|
||||
|
@ -192,11 +250,9 @@ const roomManager: IRoomManagerServer = {
|
|||
});
|
||||
},
|
||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||
socketManager.sendAdminMessage(
|
||||
call.request.getRoomid(),
|
||||
call.request.getRecipientuuid(),
|
||||
call.request.getMessage()
|
||||
);
|
||||
socketManager
|
||||
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||
.catch((e) => console.error(e));
|
||||
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
|
@ -207,26 +263,33 @@ 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());
|
||||
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 {
|
||||
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
|
||||
// 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));
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
sendWorldFullWarningToRoom(
|
||||
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
|
||||
callback: sendUnaryData<EmptyMessage>
|
||||
): void {
|
||||
socketManager.dispatchWorlFullWarning(call.request.getRoomid());
|
||||
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||
socketManager.dispatchWorldFullWarning(call.request.getRoomid()).catch((e) => console.error(e));
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
sendRefreshRoomPrompt(
|
||||
call: ServerUnaryCall<RefreshRoomPromptMessage>,
|
||||
callback: sendUnaryData<EmptyMessage>
|
||||
): void {
|
||||
socketManager.dispatchRoomRefresh(call.request.getRoomid());
|
||||
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||
socketManager.dispatchRoomRefresh(call.request.getRoomid()).catch((e) => console.error(e));
|
||||
callback(null, new EmptyMessage());
|
||||
},
|
||||
};
|
||||
|
|
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>;
|
1
back/src/Services/LocalUrlError.ts
Normal file
1
back/src/Services/LocalUrlError.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export class LocalUrlError extends Error {}
|
67
back/src/Services/MapFetcher.ts
Normal file
67
back/src/Services/MapFetcher.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
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
|
||||
timeout: 10000, // Timeout after 10 seconds
|
||||
});
|
||||
|
||||
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).bind(resolver)(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,14 @@
|
|||
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 +22,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);
|
||||
}
|
||||
|
|
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 };
|
43
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
43
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
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>>;
|
||||
private readonly hdel: OmitThisParameter<(arg1: string, arg2: string) => Promise<number>>;
|
||||
|
||||
constructor(private redisClient: RedisClient) {
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
this.hgetall = promisify(redisClient.hgetall).bind(redisClient);
|
||||
this.hset = promisify(redisClient.hset).bind(redisClient);
|
||||
this.hdel = promisify(redisClient.hdel).bind(redisClient);
|
||||
/* eslint-enable @typescript-eslint/unbound-method */
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
// The value is passed to JSON.stringify client side. If value is "undefined", JSON.stringify returns "undefined"
|
||||
// which is translated to empty string when fetching the value in the pusher.
|
||||
// Therefore, empty string server side == undefined client side.
|
||||
if (value === "") {
|
||||
return this.hdel(roomUrl, key);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -30,6 +30,9 @@ import {
|
|||
BanUserMessage,
|
||||
RefreshRoomMessage,
|
||||
EmotePromptMessage,
|
||||
VariableMessage,
|
||||
BatchToPusherRoomMessage,
|
||||
SubToPusherRoomMessage,
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import { User, UserSocket } from "../Model/User";
|
||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||
|
@ -48,7 +51,7 @@ import Jwt from "jsonwebtoken";
|
|||
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
||||
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
||||
import { gaugeManager } from "./GaugeManager";
|
||||
import { ZoneSocket } from "../RoomManager";
|
||||
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||
import { Zone } from "_Model/Zone";
|
||||
import Debug from "debug";
|
||||
import { Admin } from "_Model/Admin";
|
||||
|
@ -65,7 +68,9 @@ function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): vo
|
|||
}
|
||||
|
||||
export class SocketManager {
|
||||
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
|
||||
//private rooms = new Map<string, GameRoom>();
|
||||
// List of rooms in process of loading.
|
||||
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
||||
|
||||
constructor() {
|
||||
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
||||
|
@ -101,6 +106,16 @@ export class SocketManager {
|
|||
roomJoinedMessage.addItem(itemStateMessage);
|
||||
}
|
||||
|
||||
const variables = await room.getVariablesForTags(user.tags);
|
||||
|
||||
for (const [name, value] of variables.entries()) {
|
||||
const variableMessage = new VariableMessage();
|
||||
variableMessage.setName(name);
|
||||
variableMessage.setValue(value);
|
||||
|
||||
roomJoinedMessage.addVariable(variableMessage);
|
||||
}
|
||||
|
||||
roomJoinedMessage.setCurrentuserid(user.id);
|
||||
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
|
@ -114,30 +129,25 @@ export class SocketManager {
|
|||
}
|
||||
|
||||
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
||||
try {
|
||||
const userMoves = userMovesMessage.toObject();
|
||||
const position = userMovesMessage.getPosition();
|
||||
const userMoves = userMovesMessage.toObject();
|
||||
const position = userMovesMessage.getPosition();
|
||||
|
||||
// If CPU is high, let's drop messages of users moving (we will only dispatch the final position)
|
||||
if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (position === undefined) {
|
||||
throw new Error("Position not found in message");
|
||||
}
|
||||
const viewport = userMoves.viewport;
|
||||
if (viewport === undefined) {
|
||||
throw new Error("Viewport not found in message");
|
||||
}
|
||||
|
||||
// update position in the world
|
||||
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
||||
//room.setViewport(client, client.viewport);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "user_position" event');
|
||||
console.error(e);
|
||||
// If CPU is high, let's drop messages of users moving (we will only dispatch the final position)
|
||||
if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (position === undefined) {
|
||||
throw new Error("Position not found in message");
|
||||
}
|
||||
const viewport = userMoves.viewport;
|
||||
if (viewport === undefined) {
|
||||
throw new Error("Viewport not found in message");
|
||||
}
|
||||
|
||||
// update position in the world
|
||||
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
||||
//room.setViewport(client, client.viewport);
|
||||
}
|
||||
|
||||
// Useless now, will be useful again if we allow editing details in game
|
||||
|
@ -156,32 +166,26 @@ export class SocketManager {
|
|||
}*/
|
||||
|
||||
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
||||
try {
|
||||
room.setSilent(user, silentMessage.getSilent());
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "handleSilentMessage"');
|
||||
console.error(e);
|
||||
}
|
||||
room.setSilent(user, silentMessage.getSilent());
|
||||
}
|
||||
|
||||
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
|
||||
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
||||
|
||||
try {
|
||||
const subMessage = new SubMessage();
|
||||
subMessage.setItemeventmessage(itemEventMessage);
|
||||
const subMessage = new SubMessage();
|
||||
subMessage.setItemeventmessage(itemEventMessage);
|
||||
|
||||
// Let's send the event without using the SocketIO room.
|
||||
// TODO: move this in the GameRoom class.
|
||||
for (const user of room.getUsers().values()) {
|
||||
user.emitInBatch(subMessage);
|
||||
}
|
||||
|
||||
room.setItemState(itemEvent.itemId, itemEvent.state);
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "item_event"');
|
||||
console.error(e);
|
||||
// Let's send the event without using the SocketIO room.
|
||||
// TODO: move this in the GameRoom class.
|
||||
for (const user of room.getUsers().values()) {
|
||||
user.emitInBatch(subMessage);
|
||||
}
|
||||
|
||||
room.setItemState(itemEvent.itemId, itemEvent.state);
|
||||
}
|
||||
|
||||
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage): Promise<void> {
|
||||
return room.setVariable(variableMessage.getName(), variableMessage.getValue(), user);
|
||||
}
|
||||
|
||||
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
||||
|
@ -250,21 +254,21 @@ export class SocketManager {
|
|||
//user leave previous world
|
||||
room.leave(user);
|
||||
if (room.isEmpty()) {
|
||||
this.rooms.delete(room.roomId);
|
||||
this.roomsPromises.delete(room.roomUrl);
|
||||
gaugeManager.decNbRoomGauge();
|
||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
||||
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||
}
|
||||
} finally {
|
||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
|
||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
|
||||
console.log("A user left");
|
||||
}
|
||||
}
|
||||
|
||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
||||
//check and create new world for a room
|
||||
let world = this.rooms.get(roomId);
|
||||
if (world === undefined) {
|
||||
world = new GameRoom(
|
||||
//check and create new room
|
||||
let roomPromise = this.roomsPromises.get(roomId);
|
||||
if (roomPromise === undefined) {
|
||||
roomPromise = GameRoom.create(
|
||||
roomId,
|
||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
||||
|
@ -278,11 +282,18 @@ export class SocketManager {
|
|||
this.onClientLeave(thing, newZone, listener),
|
||||
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||
this.onEmote(emoteEventMessage, listener)
|
||||
);
|
||||
gaugeManager.incNbRoomGauge();
|
||||
this.rooms.set(roomId, world);
|
||||
)
|
||||
.then((gameRoom) => {
|
||||
gaugeManager.incNbRoomGauge();
|
||||
return gameRoom;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.roomsPromises.delete(roomId);
|
||||
throw e;
|
||||
});
|
||||
this.roomsPromises.set(roomId, roomPromise);
|
||||
}
|
||||
return Promise.resolve(world);
|
||||
return roomPromise;
|
||||
}
|
||||
|
||||
private async joinRoom(
|
||||
|
@ -308,6 +319,7 @@ export class SocketManager {
|
|||
throw new Error("clientUser.userId is not an integer " + thing.id);
|
||||
}
|
||||
userJoinedZoneMessage.setUserid(thing.id);
|
||||
userJoinedZoneMessage.setUseruuid(thing.uuid);
|
||||
userJoinedZoneMessage.setName(thing.name);
|
||||
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
||||
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
||||
|
@ -425,7 +437,6 @@ export class SocketManager {
|
|||
// Let's send 2 messages: one to the user joining the group and one to the other user
|
||||
const webrtcStartMessage1 = new WebRtcStartMessage();
|
||||
webrtcStartMessage1.setUserid(otherUser.id);
|
||||
webrtcStartMessage1.setName(otherUser.name);
|
||||
webrtcStartMessage1.setInitiator(true);
|
||||
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
|
||||
|
@ -436,14 +447,10 @@ export class SocketManager {
|
|||
const serverToClientMessage1 = new ServerToClientMessage();
|
||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||
|
||||
//if (!user.socket.disconnecting) {
|
||||
user.socket.write(serverToClientMessage1);
|
||||
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
||||
//}
|
||||
|
||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
||||
webrtcStartMessage2.setUserid(user.id);
|
||||
webrtcStartMessage2.setName(user.name);
|
||||
webrtcStartMessage2.setInitiator(false);
|
||||
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
||||
|
@ -454,10 +461,7 @@ export class SocketManager {
|
|||
const serverToClientMessage2 = new ServerToClientMessage();
|
||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
||||
|
||||
//if (!otherUser.socket.disconnecting) {
|
||||
otherUser.socket.write(serverToClientMessage2);
|
||||
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -515,21 +519,16 @@ export class SocketManager {
|
|||
}
|
||||
|
||||
emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
|
||||
try {
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
|
||||
const serverToClientMessage = new ServerToClientMessage();
|
||||
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
|
||||
|
||||
for (const [id, user] of room.getUsers().entries()) {
|
||||
user.socket.write(serverToClientMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('An error occurred on "emitPlayGlobalMessage" event');
|
||||
console.error(e);
|
||||
for (const [id, user] of room.getUsers().entries()) {
|
||||
user.socket.write(serverToClientMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public getWorlds(): Map<string, GameRoom> {
|
||||
return this.rooms;
|
||||
public getWorlds(): Map<string, PromiseLike<GameRoom>> {
|
||||
return this.roomsPromises;
|
||||
}
|
||||
|
||||
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
||||
|
@ -599,11 +598,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);
|
||||
|
@ -614,6 +612,7 @@ export class SocketManager {
|
|||
if (thing instanceof User) {
|
||||
const userJoinedMessage = new UserJoinedZoneMessage();
|
||||
userJoinedMessage.setUserid(thing.id);
|
||||
userJoinedMessage.setUseruuid(thing.uuid);
|
||||
userJoinedMessage.setName(thing.name);
|
||||
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
||||
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
||||
|
@ -643,16 +642,37 @@ 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);
|
||||
}
|
||||
|
||||
async addRoomListener(call: RoomSocket, roomId: string) {
|
||||
const room = await this.getOrCreateRoom(roomId);
|
||||
if (!room) {
|
||||
throw new Error("In addRoomListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
room.addRoomListener(call);
|
||||
|
||||
const batchMessage = new BatchToPusherRoomMessage();
|
||||
|
||||
call.write(batchMessage);
|
||||
}
|
||||
|
||||
async removeRoomListener(call: RoomSocket, roomId: string) {
|
||||
const room = await this.roomsPromises.get(roomId);
|
||||
if (!room) {
|
||||
throw new Error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
||||
}
|
||||
|
||||
room.removeRoomListener(call);
|
||||
}
|
||||
|
||||
public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise<GameRoom> {
|
||||
const room = await socketManager.getOrCreateRoom(roomId);
|
||||
|
||||
|
@ -664,14 +684,14 @@ export class SocketManager {
|
|||
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||
room.adminLeave(admin);
|
||||
if (room.isEmpty()) {
|
||||
this.rooms.delete(room.roomId);
|
||||
this.roomsPromises.delete(room.roomUrl);
|
||||
gaugeManager.decNbRoomGauge();
|
||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
||||
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 '" +
|
||||
|
@ -701,8 +721,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 '" +
|
||||
|
@ -737,8 +757,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(
|
||||
|
@ -761,8 +781,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(
|
||||
|
@ -783,8 +803,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;
|
||||
}
|
||||
|
|
218
back/src/Services/VariablesManager.ts
Normal file
218
back/src/Services/VariablesManager.ts
Normal file
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* 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";
|
||||
import { variablesRepository } from "./Repository/VariablesRepository";
|
||||
import { redisClient } from "./RedisClient";
|
||||
|
||||
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 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Let's load data from the Redis backend.
|
||||
*/
|
||||
public async init(): Promise<VariablesManager> {
|
||||
if (!this.shouldPersist()) {
|
||||
return this;
|
||||
}
|
||||
const variables = await variablesRepository.loadVariables(this.roomUrl);
|
||||
for (const key in variables) {
|
||||
// Let's only set variables if they are in the map (if the map has changed, maybe stored variables do not exist anymore)
|
||||
if (this.variableObjects) {
|
||||
const variableObject = this.variableObjects.get(key);
|
||||
if (variableObject === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (!variableObject.persist) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
this._variables.set(key, variables[key]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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") {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the variable.
|
||||
*
|
||||
* Returns who is allowed to read the variable (the readableby property) or "undefined" if anyone can read it.
|
||||
* Also, returns "false" if the variable was not modified (because we set it to the value it already has)
|
||||
*
|
||||
* @param name
|
||||
* @param value
|
||||
* @param user
|
||||
*/
|
||||
setVariable(name: string, value: string, user: User): string | undefined | false {
|
||||
let readableBy: string | undefined;
|
||||
let variableObject: Variable | undefined;
|
||||
if (this.variableObjects) {
|
||||
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.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;
|
||||
}
|
||||
|
||||
// If the value is not modified, return false
|
||||
if (this._variables.get(name) === value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._variables.set(name, value);
|
||||
|
||||
if (variableObject !== undefined && variableObject.persist) {
|
||||
variablesRepository
|
||||
.saveVariable(this.roomUrl, name, value)
|
||||
.catch((e) => console.error("Error while saving variable in Redis:", e));
|
||||
}
|
||||
|
||||
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.includes(variableObject.readableBy)) {
|
||||
readableVariables.set(key, value);
|
||||
}
|
||||
}
|
||||
return readableVariables;
|
||||
}
|
||||
}
|
|
@ -1,59 +1,62 @@
|
|||
import "jasmine";
|
||||
import {ConnectCallback, DisconnectCallback, GameRoom} from "../src/Model/GameRoom";
|
||||
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
||||
import {Group} from "../src/Model/Group";
|
||||
import {User, UserSocket} from "_Model/User";
|
||||
import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb";
|
||||
import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
|
||||
import { Point } from "../src/Model/Websocket/MessageUserPosition";
|
||||
import { Group } from "../src/Model/Group";
|
||||
import { User, UserSocket } from "_Model/User";
|
||||
import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import {EmoteCallback} from "_Model/Zone";
|
||||
import { EmoteCallback } from "_Model/Zone";
|
||||
|
||||
function createMockUser(userId: number): User {
|
||||
return {
|
||||
userId
|
||||
userId,
|
||||
} as unknown as User;
|
||||
}
|
||||
|
||||
function createMockUserSocket(): UserSocket {
|
||||
return {
|
||||
} as unknown as UserSocket;
|
||||
return {} as unknown as UserSocket;
|
||||
}
|
||||
|
||||
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage
|
||||
{
|
||||
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage {
|
||||
const positionMessage = new PositionMessage();
|
||||
positionMessage.setX(x);
|
||||
positionMessage.setY(y);
|
||||
positionMessage.setDirection(Direction.DOWN);
|
||||
positionMessage.setMoving(false);
|
||||
const joinRoomMessage = new JoinRoomMessage();
|
||||
joinRoomMessage.setUseruuid('1');
|
||||
joinRoomMessage.setIpaddress('10.0.0.2');
|
||||
joinRoomMessage.setName('foo');
|
||||
joinRoomMessage.setRoomid('_/global/test.json');
|
||||
joinRoomMessage.setUseruuid("1");
|
||||
joinRoomMessage.setIpaddress("10.0.0.2");
|
||||
joinRoomMessage.setName("foo");
|
||||
joinRoomMessage.setRoomid("_/global/test.json");
|
||||
joinRoomMessage.setPositionmessage(positionMessage);
|
||||
return joinRoomMessage;
|
||||
}
|
||||
|
||||
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
|
||||
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++;
|
||||
}
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
};
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
||||
|
||||
}
|
||||
const world = await GameRoom.create(
|
||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||
connect,
|
||||
disconnect,
|
||||
160,
|
||||
160,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
);
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
|
||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
||||
|
||||
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 500, 100));
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100));
|
||||
|
||||
world.updatePosition(user2, new Point(261, 100));
|
||||
|
||||
|
@ -67,26 +70,34 @@ 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;
|
||||
}
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
};
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
||||
|
||||
}
|
||||
const world = await GameRoom.create(
|
||||
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||
connect,
|
||||
disconnect,
|
||||
160,
|
||||
160,
|
||||
() => {},
|
||||
() => {},
|
||||
() => {},
|
||||
emote
|
||||
);
|
||||
|
||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||
|
||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 200, 100));
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 200, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
connectCalled = false;
|
||||
|
||||
// baz joins at the outer limit of the group
|
||||
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 311, 100));
|
||||
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 311, 100));
|
||||
|
||||
expect(connectCalled).toBe(false);
|
||||
|
||||
|
@ -95,31 +106,40 @@ 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 => {
|
||||
connectCalled = true;
|
||||
}
|
||||
};
|
||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||
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));
|
||||
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 259, 100));
|
||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100));
|
||||
|
||||
expect(connectCalled).toBe(true);
|
||||
expect(disconnectCallNumber).toBe(0);
|
||||
|
||||
world.updatePosition(user2, new Point(100+160+160+1, 100));
|
||||
world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100));
|
||||
|
||||
expect(disconnectCallNumber).toBe(2);
|
||||
|
||||
world.updatePosition(user2, new Point(262, 100));
|
||||
expect(disconnectCallNumber).toBe(2);
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
|
|
32
back/tests/MapFetcherTest.ts
Normal file
32
back/tests/MapFetcherTest.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { arrayIntersect } from "../src/Services/ArrayHelper";
|
||||
import { mapFetcher } from "../src/Services/MapFetcher";
|
||||
|
||||
describe("MapFetcher", () => {
|
||||
it("should return true on localhost ending URLs", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://localhost")).toBeTrue();
|
||||
expect(await mapFetcher.isLocalUrl("https://foo.localhost")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return true on DNS resolving to a local domain", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://127.0.0.1.nip.io")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return true on an IP resolving to a local domain", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://127.0.0.1")).toBeTrue();
|
||||
expect(await mapFetcher.isLocalUrl("https://192.168.0.1")).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return false on an IP resolving to a global domain", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://51.12.42.42")).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return false on an DNS resolving to a global domain", async () => {
|
||||
expect(await mapFetcher.isLocalUrl("https://maps.workadventu.re")).toBeFalse();
|
||||
});
|
||||
|
||||
it("should throw error on invalid domain", async () => {
|
||||
await expectAsync(
|
||||
mapFetcher.isLocalUrl("https://this.domain.name.doesnotexistfoobgjkgfdjkgldf.com")
|
||||
).toBeRejected();
|
||||
});
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
|
||||
|
||||
describe("RoomIdentifier", () => {
|
||||
it("should flag public id as anonymous", () => {
|
||||
expect(isRoomAnonymous('_/global/test')).toBe(true);
|
||||
});
|
||||
it("should flag public id as not anonymous", () => {
|
||||
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
|
||||
});
|
||||
it("should extract roomSlug from public ID", () => {
|
||||
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
|
||||
});
|
||||
it("should extract correct from private ID", () => {
|
||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
|
||||
expect(organizationSlug).toBe('afup');
|
||||
expect(worldSlug).toBe('afup2020');
|
||||
expect(roomSlug).toBe('1floor');
|
||||
});
|
||||
})
|
|
@ -3,7 +3,7 @@
|
|||
"experimentalDecorators": true,
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"downlevelIteration": true,
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
|
|
|
@ -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"
|
||||
|
@ -187,6 +194,13 @@
|
|||
semver "^7.3.2"
|
||||
tsutils "^3.17.1"
|
||||
|
||||
"@workadventure/tiled-map-type-guard@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba"
|
||||
integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A==
|
||||
dependencies:
|
||||
generic-type-guard "^3.4.1"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
@ -797,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"
|
||||
|
@ -1181,6 +1200,11 @@ generic-type-guard@^3.2.0:
|
|||
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.3.3.tgz#954b846fecff91047cadb0dcc28930811fcb9dc1"
|
||||
integrity sha512-SXraZvNW/uTfHVgB48iEwWaD1XFJ1nvZ8QP6qy9pSgaScEyQqFHYN5E6d6rCsJgrvlWKygPrNum7QeJHegzNuQ==
|
||||
|
||||
generic-type-guard@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.4.1.tgz#0896dc018de915c890562a34763858076e4676da"
|
||||
integrity sha512-sXce0Lz3Wfy2rR1W8O8kUemgEriTeG1x8shqSJeWGb0FwJu2qBEkB1M2qXbdSLmpgDnHcIXo0Dj/1VLNJkK/QA==
|
||||
|
||||
get-own-enumerable-property-symbols@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
|
||||
|
@ -1417,6 +1441,11 @@ invert-kv@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
||||
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
|
||||
|
||||
ipaddr.js@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0"
|
||||
integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==
|
||||
|
||||
is-accessor-descriptor@^0.1.6:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
|
||||
|
@ -2424,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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue