Merge pull request #1285 from thecodingmachine/mapDetailsByRoomId
Migrating away from the notion of public/private URL in WorkAdventure Github repository
This commit is contained in:
commit
510477b99b
@ -23,6 +23,10 @@
|
|||||||
- The chat allows your to see the visit card of users
|
- The chat allows your to see the visit card of users
|
||||||
- You can close the chat window with the escape key
|
- You can close the chat window with the escape key
|
||||||
- Added a 'Enable notifications' button in the menu.
|
- Added a 'Enable notifications' button in the menu.
|
||||||
|
- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed:
|
||||||
|
- `/api/map`: now accepts a complete room URL instead of organization/world/room slugs
|
||||||
|
- `/api/ban`: new endpoint to report users
|
||||||
|
- as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing
|
||||||
|
|
||||||
## Version 1.4.3 - 1.4.4 - 1.4.5
|
## Version 1.4.3 - 1.4.4 - 1.4.5
|
||||||
|
|
||||||
|
@ -5,8 +5,6 @@ import { PositionInterface } from "_Model/PositionInterface";
|
|||||||
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||||
import { PositionNotifier } from "./PositionNotifier";
|
import { PositionNotifier } from "./PositionNotifier";
|
||||||
import { Movable } from "_Model/Movable";
|
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 { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import { ZoneSocket } from "src/RoomManager";
|
import { ZoneSocket } from "src/RoomManager";
|
||||||
@ -31,15 +29,12 @@ export class GameRoom {
|
|||||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
||||||
|
|
||||||
private readonly positionNotifier: PositionNotifier;
|
private readonly positionNotifier: PositionNotifier;
|
||||||
public readonly roomId: string;
|
public readonly roomUrl: string;
|
||||||
public readonly roomSlug: string;
|
|
||||||
public readonly worldSlug: string = "";
|
|
||||||
public readonly organizationSlug: string = "";
|
|
||||||
private versionNumber: number = 1;
|
private versionNumber: number = 1;
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
roomId: string,
|
roomUrl: string,
|
||||||
connectCallback: ConnectCallback,
|
connectCallback: ConnectCallback,
|
||||||
disconnectCallback: DisconnectCallback,
|
disconnectCallback: DisconnectCallback,
|
||||||
minDistance: number,
|
minDistance: number,
|
||||||
@ -49,16 +44,7 @@ export class GameRoom {
|
|||||||
onLeaves: LeavesCallback,
|
onLeaves: LeavesCallback,
|
||||||
onEmote: EmoteCallback
|
onEmote: EmoteCallback
|
||||||
) {
|
) {
|
||||||
this.roomId = roomId;
|
this.roomUrl = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.users = new Map<number, User>();
|
this.users = new Map<number, User>();
|
||||||
this.usersByUuid = new Map<string, User>();
|
this.usersByUuid = new Map<string, User>();
|
||||||
@ -177,7 +163,7 @@ export class GameRoom {
|
|||||||
} else {
|
} else {
|
||||||
const closestUser: User = closestItem;
|
const closestUser: User = closestItem;
|
||||||
const group: Group = new Group(
|
const group: Group = new Group(
|
||||||
this.roomId,
|
this.roomUrl,
|
||||||
[user, closestUser],
|
[user, closestUser],
|
||||||
this.connectCallback,
|
this.connectCallback,
|
||||||
this.disconnectCallback,
|
this.disconnectCallback,
|
||||||
|
@ -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 };
|
|
||||||
};
|
|
@ -250,12 +250,12 @@ export class SocketManager {
|
|||||||
//user leave previous world
|
//user leave previous world
|
||||||
room.leave(user);
|
room.leave(user);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomId);
|
this.rooms.delete(room.roomUrl);
|
||||||
gaugeManager.decNbRoomGauge();
|
gaugeManager.decNbRoomGauge();
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
|
clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
|
||||||
console.log("A user left");
|
console.log("A user left");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -658,9 +658,9 @@ export class SocketManager {
|
|||||||
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||||
room.adminLeave(admin);
|
room.adminLeave(admin);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomId);
|
this.rooms.delete(room.roomUrl);
|
||||||
gaugeManager.decNbRoomGauge();
|
gaugeManager.decNbRoomGauge();
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
})
|
|
@ -38,11 +38,9 @@ class ConnectionManager {
|
|||||||
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
|
|
||||||
const organizationSlug = data.organizationSlug;
|
const roomUrl = data.roomUrl;
|
||||||
const worldSlug = data.worldSlug;
|
|
||||||
const roomSlug = data.roomSlug;
|
|
||||||
|
|
||||||
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash);
|
const room = await Room.createRoom(new URL(window.location.protocol + '//' + window.location.host + roomUrl + window.location.search + window.location.hash));
|
||||||
urlManager.pushRoomIdToUrl(room);
|
urlManager.pushRoomIdToUrl(room);
|
||||||
return Promise.resolve(room);
|
return Promise.resolve(room);
|
||||||
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
|
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
|
||||||
@ -66,22 +64,21 @@ class ConnectionManager {
|
|||||||
throw "Error to store local user data";
|
throw "Error to store local user data";
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomId: string;
|
let roomPath: string;
|
||||||
if (connexionType === GameConnexionTypes.empty) {
|
if (connexionType === GameConnexionTypes.empty) {
|
||||||
roomId = START_ROOM_URL;
|
roomPath = window.location.protocol + '//' + window.location.host + START_ROOM_URL;
|
||||||
} else {
|
} else {
|
||||||
roomId = window.location.pathname + window.location.search + window.location.hash;
|
roomPath = window.location.protocol + '//' + window.location.host + window.location.pathname + window.location.search + window.location.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
//get detail map for anonymous login and set texture in local storage
|
//get detail map for anonymous login and set texture in local storage
|
||||||
const room = new Room(roomId);
|
const room = await Room.createRoom(new URL(roomPath));
|
||||||
const mapDetail = await room.getMapDetail();
|
if(room.textures != undefined && room.textures.length > 0) {
|
||||||
if(mapDetail.textures != undefined && mapDetail.textures.length > 0) {
|
|
||||||
//check if texture was changed
|
//check if texture was changed
|
||||||
if(localUser.textures.length === 0){
|
if(localUser.textures.length === 0){
|
||||||
localUser.textures = mapDetail.textures;
|
localUser.textures = room.textures;
|
||||||
}else{
|
}else{
|
||||||
mapDetail.textures.forEach((newTexture) => {
|
room.textures.forEach((newTexture) => {
|
||||||
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
||||||
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
|
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
|
||||||
return;
|
return;
|
||||||
@ -114,9 +111,9 @@ class ConnectionManager {
|
|||||||
this.localUser = new LocalUser('', 'test', []);
|
this.localUser = new LocalUser('', 'test', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
|
public connectToRoomSocket(roomUrl: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
|
||||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||||
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion);
|
const connection = new RoomConnection(this.localUser.jwtToken, roomUrl, name, characterLayers, position, viewport, companion);
|
||||||
connection.onConnectError((error: object) => {
|
connection.onConnectError((error: object) => {
|
||||||
console.log('An error occurred while connecting to socket server. Retrying');
|
console.log('An error occurred while connecting to socket server. Retrying');
|
||||||
reject(error);
|
reject(error);
|
||||||
@ -137,7 +134,7 @@ class ConnectionManager {
|
|||||||
this.reconnectingTimeout = setTimeout(() => {
|
this.reconnectingTimeout = setTimeout(() => {
|
||||||
//todo: allow a way to break recursion?
|
//todo: allow a way to break recursion?
|
||||||
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
|
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
|
||||||
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
|
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
|
||||||
}, 4000 + Math.floor(Math.random() * 2000) );
|
}, 4000 + Math.floor(Math.random() * 2000) );
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,91 +3,103 @@ import { PUSHER_URL } from "../Enum/EnvironmentVariable";
|
|||||||
import type { CharacterTexture } from "./LocalUser";
|
import type { CharacterTexture } from "./LocalUser";
|
||||||
|
|
||||||
export class MapDetail {
|
export class MapDetail {
|
||||||
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {
|
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RoomRedirect {
|
||||||
|
redirectUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Room {
|
export class Room {
|
||||||
public readonly id: string;
|
public readonly id: string;
|
||||||
public readonly isPublic: boolean;
|
public readonly isPublic: boolean;
|
||||||
private mapUrl: string | undefined;
|
private _mapUrl: string | undefined;
|
||||||
private textures: CharacterTexture[] | undefined;
|
private _textures: CharacterTexture[] | undefined;
|
||||||
private instance: string | undefined;
|
private instance: string | undefined;
|
||||||
private _search: URLSearchParams;
|
private readonly _search: URLSearchParams;
|
||||||
|
|
||||||
constructor(id: string) {
|
private constructor(private roomUrl: URL) {
|
||||||
const url = new URL(id, 'https://example.com');
|
this.id = roomUrl.pathname;
|
||||||
|
|
||||||
this.id = url.pathname;
|
if (this.id.startsWith("/")) {
|
||||||
|
|
||||||
if (this.id.startsWith('/')) {
|
|
||||||
this.id = this.id.substr(1);
|
this.id = this.id.substr(1);
|
||||||
}
|
}
|
||||||
if (this.id.startsWith('_/')) {
|
if (this.id.startsWith("_/")) {
|
||||||
this.isPublic = true;
|
this.isPublic = true;
|
||||||
} else if (this.id.startsWith('@/')) {
|
} else if (this.id.startsWith("@/")) {
|
||||||
this.isPublic = false;
|
this.isPublic = false;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid room ID');
|
throw new Error("Invalid room ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._search = new URLSearchParams(url.search);
|
this._search = new URLSearchParams(roomUrl.search);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): { roomId: string, hash: string | null } {
|
/**
|
||||||
let roomId = '';
|
* Creates a "Room" object representing the room.
|
||||||
let hash = null;
|
* This method will follow room redirects if necessary, so the instance returned is a "real" room.
|
||||||
if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link
|
*/
|
||||||
//Relative identifier can be deep enough to rewrite the base domain, so we cannot use the variable 'baseUrl' as the actual base url for the URL objects.
|
public static async createRoom(roomUrl: URL): Promise<Room> {
|
||||||
//We instead use 'workadventure' as a dummy base value.
|
let redirectCount = 0;
|
||||||
const baseUrlObject = new URL(baseUrl);
|
while (redirectCount < 32) {
|
||||||
const absoluteExitSceneUrl = new URL(identifier, 'http://workadventure/_/' + currentInstance + '/' + baseUrlObject.hostname + baseUrlObject.pathname);
|
const room = new Room(roomUrl);
|
||||||
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
|
const result = await room.getMapDetail();
|
||||||
roomId = roomId.substring(1); //remove the leading slash
|
if (result instanceof MapDetail) {
|
||||||
hash = absoluteExitSceneUrl.hash;
|
return room;
|
||||||
hash = hash.substring(1); //remove the leading diese
|
|
||||||
if (!hash.length) {
|
|
||||||
hash = null
|
|
||||||
}
|
|
||||||
} else { //absolute room Id
|
|
||||||
const parts = identifier.split('#');
|
|
||||||
roomId = parts[0];
|
|
||||||
roomId = roomId.substring(1); //remove the leading slash
|
|
||||||
if (parts.length > 1) {
|
|
||||||
hash = parts[1]
|
|
||||||
}
|
}
|
||||||
|
redirectCount++;
|
||||||
|
roomUrl = new URL(result.redirectUrl);
|
||||||
}
|
}
|
||||||
return { roomId, hash }
|
throw new Error("Room resolving seems stuck in a redirect loop after 32 redirect attempts");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMapDetail(): Promise<MapDetail> {
|
public static getRoomPathFromExitUrl(exitUrl: string, currentRoomUrl: string): URL {
|
||||||
return new Promise<MapDetail>((resolve, reject) => {
|
const url = new URL(exitUrl, currentRoomUrl);
|
||||||
if (this.mapUrl !== undefined && this.textures != undefined) {
|
return url;
|
||||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isPublic) {
|
/**
|
||||||
const match = /_\/[^/]+\/(.+)/.exec(this.id);
|
* @deprecated USage of exitSceneUrl is deprecated and therefore, this method is deprecated too.
|
||||||
if (!match) throw new Error('Could not extract url from "' + this.id + '"');
|
*/
|
||||||
this.mapUrl = window.location.protocol + '//' + match[1];
|
public static getRoomPathFromExitSceneUrl(
|
||||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
exitSceneUrl: string,
|
||||||
return;
|
currentRoomUrl: string,
|
||||||
} else {
|
currentMapUrl: string
|
||||||
// We have a private ID, we need to query the map URL from the server.
|
): URL {
|
||||||
const urlParts = this.parsePrivateUrl(this.id);
|
const absoluteExitSceneUrl = new URL(exitSceneUrl, currentMapUrl);
|
||||||
|
const baseUrl = new URL(currentRoomUrl);
|
||||||
|
|
||||||
Axios.get(`${PUSHER_URL}/map`, {
|
const currentRoom = new Room(baseUrl);
|
||||||
params: urlParts
|
let instance: string = "global";
|
||||||
}).then(({ data }) => {
|
if (currentRoom.isPublic) {
|
||||||
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
|
instance = currentRoom.instance as string;
|
||||||
resolve(data);
|
}
|
||||||
return;
|
|
||||||
}).catch((reason) => {
|
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
|
||||||
reject(reason);
|
if (absoluteExitSceneUrl.hash) {
|
||||||
});
|
baseUrl.hash = absoluteExitSceneUrl.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
|
||||||
|
const result = await Axios.get(`${PUSHER_URL}/map`, {
|
||||||
|
params: {
|
||||||
|
playUri: this.roomUrl.toString(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = result.data;
|
||||||
|
if (data.redirectUrl) {
|
||||||
|
return {
|
||||||
|
redirectUrl: data.redirectUrl as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
|
||||||
|
this._mapUrl = data.mapUrl;
|
||||||
|
this._textures = data.textures;
|
||||||
|
return new MapDetail(data.mapUrl, data.textures);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,21 +120,24 @@ export class Room {
|
|||||||
} else {
|
} else {
|
||||||
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
|
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
|
||||||
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
|
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
|
||||||
this.instance = match[1] + '/' + match[2];
|
this.instance = match[1] + "/" + match[2];
|
||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parsePrivateUrl(url: string): { organizationSlug: string, worldSlug: string, roomSlug?: string } {
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
private parsePrivateUrl(url: string): { organizationSlug: string; worldSlug: string; roomSlug?: string } {
|
||||||
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
|
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
|
||||||
const match = regex.exec(url);
|
const match = regex.exec(url);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error('Invalid URL ' + url);
|
throw new Error("Invalid URL " + url);
|
||||||
}
|
}
|
||||||
const results: { organizationSlug: string, worldSlug: string, roomSlug?: string } = {
|
const results: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
|
||||||
organizationSlug: match[1],
|
organizationSlug: match[1],
|
||||||
worldSlug: match[2],
|
worldSlug: match[2],
|
||||||
}
|
};
|
||||||
if (match[3] !== undefined) {
|
if (match[3] !== undefined) {
|
||||||
results.roomSlug = match[3];
|
results.roomSlug = match[3];
|
||||||
}
|
}
|
||||||
@ -130,8 +145,8 @@ export class Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isDisconnected(): boolean {
|
public isDisconnected(): boolean {
|
||||||
const alone = this._search.get('alone');
|
const alone = this._search.get("alone");
|
||||||
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
|
if (alone && alone !== "0" && alone.toLowerCase() !== "false") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -140,4 +155,32 @@ export class Room {
|
|||||||
public get search(): URLSearchParams {
|
public get search(): URLSearchParams {
|
||||||
return this._search;
|
return this._search;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2 rooms are equal if they share the same path (but not necessarily the same hash)
|
||||||
|
* @param room
|
||||||
|
*/
|
||||||
|
public isEqual(room: Room): boolean {
|
||||||
|
return room.key === this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key representing this room
|
||||||
|
*/
|
||||||
|
public get key(): string {
|
||||||
|
const newUrl = new URL(this.roomUrl.toString());
|
||||||
|
newUrl.hash = "";
|
||||||
|
return newUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
get textures(): CharacterTexture[] | undefined {
|
||||||
|
return this._textures;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mapUrl(): string {
|
||||||
|
if (!this._mapUrl) {
|
||||||
|
throw new Error("Map URL not fetched yet");
|
||||||
|
}
|
||||||
|
return this._mapUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,11 +75,11 @@ export class RoomConnection implements RoomConnection {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param token A JWT token containing the UUID of the user
|
* @param token A JWT token containing the UUID of the user
|
||||||
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
|
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
token: string | null,
|
token: string | null,
|
||||||
roomId: string,
|
roomUrl: string,
|
||||||
name: string,
|
name: string,
|
||||||
characterLayers: string[],
|
characterLayers: string[],
|
||||||
position: PositionInterface,
|
position: PositionInterface,
|
||||||
@ -92,7 +92,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
url += "/";
|
url += "/";
|
||||||
}
|
}
|
||||||
url += "room";
|
url += "room";
|
||||||
url += "?roomId=" + (roomId ? encodeURIComponent(roomId) : "");
|
url += "?roomId=" + encodeURIComponent(roomUrl);
|
||||||
url += "&token=" + (token ? encodeURIComponent(token) : "");
|
url += "&token=" + (token ? encodeURIComponent(token) : "");
|
||||||
url += "&name=" + encodeURIComponent(name);
|
url += "&name=" + encodeURIComponent(name);
|
||||||
for (const layer of characterLayers) {
|
for (const layer of characterLayers) {
|
||||||
|
@ -28,7 +28,7 @@ export class GameManager {
|
|||||||
|
|
||||||
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
|
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
|
||||||
this.startRoom = await connectionManager.initGameConnexion();
|
this.startRoom = await connectionManager.initGameConnexion();
|
||||||
await this.loadMap(this.startRoom, scenePlugin);
|
this.loadMap(this.startRoom, scenePlugin);
|
||||||
|
|
||||||
if (!this.playerName) {
|
if (!this.playerName) {
|
||||||
return LoginSceneName;
|
return LoginSceneName;
|
||||||
@ -68,20 +68,19 @@ export class GameManager {
|
|||||||
return this.companion;
|
return this.companion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> {
|
public loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin) {
|
||||||
const roomID = room.id;
|
const roomID = room.key;
|
||||||
const mapDetail = await room.getMapDetail();
|
|
||||||
|
|
||||||
const gameIndex = scenePlugin.getIndex(roomID);
|
const gameIndex = scenePlugin.getIndex(roomID);
|
||||||
if (gameIndex === -1) {
|
if (gameIndex === -1) {
|
||||||
const game: Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
|
const game: Phaser.Scene = new GameScene(room, room.mapUrl);
|
||||||
scenePlugin.add(roomID, game, false);
|
scenePlugin.add(roomID, game, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
|
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
|
||||||
console.log("starting " + (this.currentGameSceneName || this.startRoom.id));
|
console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
|
||||||
scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
|
scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
|
||||||
scenePlugin.launch(MenuSceneName);
|
scenePlugin.launch(MenuSceneName);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -173,7 +173,7 @@ export class GameScene extends DirtyScene {
|
|||||||
private chatVisibilityUnsubscribe!: () => void;
|
private chatVisibilityUnsubscribe!: () => void;
|
||||||
private biggestAvailableAreaStoreUnsubscribe!: () => void;
|
private biggestAvailableAreaStoreUnsubscribe!: () => void;
|
||||||
MapUrlFile: string;
|
MapUrlFile: string;
|
||||||
RoomId: string;
|
roomUrl: string;
|
||||||
instance: string;
|
instance: string;
|
||||||
|
|
||||||
currentTick!: number;
|
currentTick!: number;
|
||||||
@ -206,14 +206,14 @@ export class GameScene extends DirtyScene {
|
|||||||
|
|
||||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||||
super({
|
super({
|
||||||
key: customKey ?? room.id,
|
key: customKey ?? room.key,
|
||||||
});
|
});
|
||||||
this.Terrains = [];
|
this.Terrains = [];
|
||||||
this.groups = new Map<number, Sprite>();
|
this.groups = new Map<number, Sprite>();
|
||||||
this.instance = room.getInstance();
|
this.instance = room.getInstance();
|
||||||
|
|
||||||
this.MapUrlFile = MapUrlFile;
|
this.MapUrlFile = MapUrlFile;
|
||||||
this.RoomId = room.id;
|
this.roomUrl = room.key;
|
||||||
|
|
||||||
this.createPromise = new Promise<void>((resolve, reject): void => {
|
this.createPromise = new Promise<void>((resolve, reject): void => {
|
||||||
this.createPromiseResolve = resolve;
|
this.createPromiseResolve = resolve;
|
||||||
@ -465,11 +465,13 @@ export class GameScene extends DirtyScene {
|
|||||||
if (layer.type === "tilelayer") {
|
if (layer.type === "tilelayer") {
|
||||||
const exitSceneUrl = this.getExitSceneUrl(layer);
|
const exitSceneUrl = this.getExitSceneUrl(layer);
|
||||||
if (exitSceneUrl !== undefined) {
|
if (exitSceneUrl !== undefined) {
|
||||||
this.loadNextGame(exitSceneUrl);
|
this.loadNextGame(
|
||||||
|
Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const exitUrl = this.getExitUrl(layer);
|
const exitUrl = this.getExitUrl(layer);
|
||||||
if (exitUrl !== undefined) {
|
if (exitUrl !== undefined) {
|
||||||
this.loadNextGame(exitUrl);
|
this.loadNextGameFromExitUrl(exitUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (layer.type === "objectgroup") {
|
if (layer.type === "objectgroup") {
|
||||||
@ -482,7 +484,7 @@ export class GameScene extends DirtyScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.gameMap.exitUrls.forEach((exitUrl) => {
|
this.gameMap.exitUrls.forEach((exitUrl) => {
|
||||||
this.loadNextGame(exitUrl);
|
this.loadNextGameFromExitUrl(exitUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.startPositionCalculator = new StartPositionCalculator(
|
this.startPositionCalculator = new StartPositionCalculator(
|
||||||
@ -587,7 +589,7 @@ export class GameScene extends DirtyScene {
|
|||||||
|
|
||||||
connectionManager
|
connectionManager
|
||||||
.connectToRoomSocket(
|
.connectToRoomSocket(
|
||||||
this.RoomId,
|
this.roomUrl,
|
||||||
this.playerName,
|
this.playerName,
|
||||||
this.characterLayers,
|
this.characterLayers,
|
||||||
{
|
{
|
||||||
@ -775,10 +777,13 @@ export class GameScene extends DirtyScene {
|
|||||||
|
|
||||||
private triggerOnMapLayerPropertyChange() {
|
private triggerOnMapLayerPropertyChange() {
|
||||||
this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => {
|
this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => {
|
||||||
if (newValue) this.onMapExit(newValue as string);
|
if (newValue)
|
||||||
|
this.onMapExit(
|
||||||
|
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => {
|
this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => {
|
||||||
if (newValue) this.onMapExit(newValue as string);
|
if (newValue) this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()));
|
||||||
});
|
});
|
||||||
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
|
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
|
||||||
if (newValue === undefined) {
|
if (newValue === undefined) {
|
||||||
@ -1003,9 +1008,9 @@ ${escapedMessage}
|
|||||||
);
|
);
|
||||||
this.iframeSubscriptionList.push(
|
this.iframeSubscriptionList.push(
|
||||||
iframeListener.loadPageStream.subscribe((url: string) => {
|
iframeListener.loadPageStream.subscribe((url: string) => {
|
||||||
this.loadNextGame(url).then(() => {
|
this.loadNextGameFromExitUrl(url).then(() => {
|
||||||
this.events.once(EVENT_TYPE.POST_UPDATE, () => {
|
this.events.once(EVENT_TYPE.POST_UPDATE, () => {
|
||||||
this.onMapExit(url);
|
this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -1060,7 +1065,7 @@ ${escapedMessage}
|
|||||||
startLayerName: this.startPositionCalculator.startLayerName,
|
startLayerName: this.startPositionCalculator.startLayerName,
|
||||||
uuid: localUserStore.getLocalUser()?.uuid,
|
uuid: localUserStore.getLocalUser()?.uuid,
|
||||||
nickname: localUserStore.getName(),
|
nickname: localUserStore.getName(),
|
||||||
roomId: this.RoomId,
|
roomId: this.roomUrl,
|
||||||
tags: this.connection ? this.connection.getAllTags() : [],
|
tags: this.connection ? this.connection.getAllTags() : [],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -1084,7 +1089,7 @@ ${escapedMessage}
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (propertyName === "exitUrl" && typeof propertyValue === "string") {
|
if (propertyName === "exitUrl" && typeof propertyValue === "string") {
|
||||||
this.loadNextGame(propertyValue);
|
this.loadNextGameFromExitUrl(propertyValue);
|
||||||
}
|
}
|
||||||
if (layer.properties === undefined) {
|
if (layer.properties === undefined) {
|
||||||
layer.properties = [];
|
layer.properties = [];
|
||||||
@ -1131,28 +1136,38 @@ ${escapedMessage}
|
|||||||
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
|
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMapExit(exitKey: string) {
|
private async onMapExit(roomUrl: URL) {
|
||||||
if (this.mapTransitioning) return;
|
if (this.mapTransitioning) return;
|
||||||
this.mapTransitioning = true;
|
this.mapTransitioning = true;
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance);
|
|
||||||
if (!roomId) throw new Error("Could not find the room from its exit key: " + exitKey);
|
let targetRoom: Room;
|
||||||
if (hash) {
|
try {
|
||||||
urlManager.pushStartLayerNameToUrl(hash);
|
targetRoom = await Room.createRoom(roomUrl);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
|
||||||
|
this.mapTransitioning = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (roomUrl.hash) {
|
||||||
|
urlManager.pushStartLayerNameToUrl(roomUrl.hash);
|
||||||
|
}
|
||||||
|
|
||||||
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
|
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
|
||||||
menuScene.reset();
|
menuScene.reset();
|
||||||
if (roomId !== this.scene.key) {
|
|
||||||
if (this.scene.get(roomId) === null) {
|
if (!targetRoom.isEqual(this.room)) {
|
||||||
console.error("next room not loaded", exitKey);
|
if (this.scene.get(targetRoom.key) === null) {
|
||||||
|
console.error("next room not loaded", targetRoom.key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cleanupClosingScene();
|
this.cleanupClosingScene();
|
||||||
this.scene.stop();
|
this.scene.stop();
|
||||||
|
this.scene.start(targetRoom.key);
|
||||||
this.scene.remove(this.scene.key);
|
this.scene.remove(this.scene.key);
|
||||||
this.scene.start(roomId);
|
|
||||||
} else {
|
} else {
|
||||||
//if the exit points to the current map, we simply teleport the user back to the startLayer
|
//if the exit points to the current map, we simply teleport the user back to the startLayer
|
||||||
this.startPositionCalculator.initPositionFromLayerName(hash, hash);
|
this.startPositionCalculator.initPositionFromLayerName(roomUrl.hash, roomUrl.hash);
|
||||||
this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
|
this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
|
||||||
this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
|
this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
|
||||||
setTimeout(() => (this.mapTransitioning = false), 500);
|
setTimeout(() => (this.mapTransitioning = false), 500);
|
||||||
@ -1244,11 +1259,18 @@ ${escapedMessage}
|
|||||||
.map((property) => property.value);
|
.map((property) => property.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadNextGameFromExitUrl(exitUrl: string): Promise<void> {
|
||||||
|
return this.loadNextGame(Room.getRoomPathFromExitUrl(exitUrl, window.location.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
//todo: push that into the gameManager
|
//todo: push that into the gameManager
|
||||||
private loadNextGame(exitSceneIdentifier: string): Promise<void> {
|
private async loadNextGame(exitRoomPath: URL): Promise<void> {
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
|
try {
|
||||||
const room = new Room(roomId);
|
const room = await Room.createRoom(exitRoomPath);
|
||||||
return gameManager.loadMap(room, this.scene).catch(() => {});
|
return gameManager.loadMap(room, this.scene);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo: in a dedicated class/function?
|
//todo: in a dedicated class/function?
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import "jasmine";
|
|
||||||
import { Room } from "../../../src/Connexion/Room";
|
|
||||||
|
|
||||||
describe("Room getIdFromIdentifier()", () => {
|
|
||||||
it("should work with an absolute room id and no hash as parameter", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', '', '');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual(null);
|
|
||||||
});
|
|
||||||
it("should work with an absolute room id and a hash as parameters", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json#start', '', '');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual("start");
|
|
||||||
});
|
|
||||||
it("should work with an absolute room id, regardless of baseUrl or instance", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', 'https://another.domain/_/global/test.json', 'lol');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("should work with a relative file link and no hash as parameters", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('./test2.json', 'https://maps.workadventu.re/test.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual(null);
|
|
||||||
});
|
|
||||||
it("should work with a relative file link with no dot", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('test2.json', 'https://maps.workadventu.re/test.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual(null);
|
|
||||||
});
|
|
||||||
it("should work with a relative file link two levels deep", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('../floor1/Floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/floor1/Floor1.json');
|
|
||||||
expect(hash).toEqual(null);
|
|
||||||
});
|
|
||||||
it("should work with a relative file link that rewrite the map domain", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('../../maps.workadventure.localhost/Floor1/floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventure.localhost/Floor1/floor1.json');
|
|
||||||
expect(hash).toEqual(null);
|
|
||||||
});
|
|
||||||
it("should work with a relative file link that rewrite the map instance", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('../../../notglobal/maps.workadventu.re/Floor1/floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/notglobal/maps.workadventu.re/Floor1/floor1.json');
|
|
||||||
expect(hash).toEqual(null);
|
|
||||||
});
|
|
||||||
it("should work with a relative file link that change the map type", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('../../../../@/tcm/is/great', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
|
|
||||||
expect(roomId).toEqual('@/tcm/is/great');
|
|
||||||
expect(hash).toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with a relative file link and a hash as parameters", () => {
|
|
||||||
const { roomId, hash } = Room.getIdFromIdentifier('./test2.json#start', 'https://maps.workadventu.re/test.json', 'global');
|
|
||||||
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
|
|
||||||
expect(hash).toEqual("start");
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,11 +1,4 @@
|
|||||||
{ "compressionlevel":-1,
|
{ "compressionlevel":-1,
|
||||||
"editorsettings":
|
|
||||||
{
|
|
||||||
"export":
|
|
||||||
{
|
|
||||||
"target":"."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"height":26,
|
"height":26,
|
||||||
"infinite":false,
|
"infinite":false,
|
||||||
"layers":[
|
"layers":[
|
||||||
@ -101,7 +94,7 @@
|
|||||||
"opacity":1,
|
"opacity":1,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"exitSceneUrl",
|
"name":"exitUrl",
|
||||||
"type":"string",
|
"type":"string",
|
||||||
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs"
|
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs"
|
||||||
}],
|
}],
|
||||||
@ -119,7 +112,7 @@
|
|||||||
"opacity":1,
|
"opacity":1,
|
||||||
"properties":[
|
"properties":[
|
||||||
{
|
{
|
||||||
"name":"exitSceneUrl",
|
"name":"exitUrl",
|
||||||
"type":"string",
|
"type":"string",
|
||||||
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs-secours"
|
"value":"\/@\/tcm\/workadventure\/floor2#down-the-stairs-secours"
|
||||||
}],
|
}],
|
||||||
@ -264,7 +257,7 @@
|
|||||||
"nextobjectid":1,
|
"nextobjectid":1,
|
||||||
"orientation":"orthogonal",
|
"orientation":"orthogonal",
|
||||||
"renderorder":"right-down",
|
"renderorder":"right-down",
|
||||||
"tiledversion":"1.3.3",
|
"tiledversion":"2021.03.23",
|
||||||
"tileheight":32,
|
"tileheight":32,
|
||||||
"tilesets":[
|
"tilesets":[
|
||||||
{
|
{
|
||||||
@ -1959,6 +1952,6 @@
|
|||||||
}],
|
}],
|
||||||
"tilewidth":32,
|
"tilewidth":32,
|
||||||
"type":"map",
|
"type":"map",
|
||||||
"version":1.2,
|
"version":1.5,
|
||||||
"width":46
|
"width":46
|
||||||
}
|
}
|
@ -39,9 +39,7 @@ export class AuthenticateController extends BaseController {
|
|||||||
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
|
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
|
||||||
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
||||||
const userUuid = data.userUuid;
|
const userUuid = data.userUuid;
|
||||||
const organizationSlug = data.organizationSlug;
|
const roomUrl = data.roomUrl;
|
||||||
const worldSlug = data.worldSlug;
|
|
||||||
const roomSlug = data.roomSlug;
|
|
||||||
const mapUrlStart = data.mapUrlStart;
|
const mapUrlStart = data.mapUrlStart;
|
||||||
const textures = data.textures;
|
const textures = data.textures;
|
||||||
|
|
||||||
@ -52,9 +50,7 @@ export class AuthenticateController extends BaseController {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
authToken,
|
authToken,
|
||||||
userUuid,
|
userUuid,
|
||||||
organizationSlug,
|
roomUrl,
|
||||||
worldSlug,
|
|
||||||
roomSlug,
|
|
||||||
mapUrlStart,
|
mapUrlStart,
|
||||||
organizationMemberToken,
|
organizationMemberToken,
|
||||||
textures,
|
textures,
|
||||||
|
@ -221,14 +221,12 @@ export class IoSocketController {
|
|||||||
memberVisitCardUrl = userData.visitCardUrl;
|
memberVisitCardUrl = userData.visitCardUrl;
|
||||||
memberTextures = userData.textures;
|
memberTextures = userData.textures;
|
||||||
if (
|
if (
|
||||||
!room.public &&
|
|
||||||
room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY &&
|
room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY &&
|
||||||
(userData.anonymous === true || !room.canAccess(memberTags))
|
(userData.anonymous === true || !room.canAccess(memberTags))
|
||||||
) {
|
) {
|
||||||
throw new Error("Insufficient privileges to access this room");
|
throw new Error("Insufficient privileges to access this room");
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!room.public &&
|
|
||||||
room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY &&
|
room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY &&
|
||||||
userData.anonymous === true
|
userData.anonymous === true
|
||||||
) {
|
) {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
|
||||||
import { BaseController } from "./BaseController";
|
import { BaseController } from "./BaseController";
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { adminApi } from "../Services/AdminApi";
|
import { adminApi, MapDetailsData } from "../Services/AdminApi";
|
||||||
|
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
|
||||||
|
|
||||||
export class MapController extends BaseController {
|
export class MapController extends BaseController {
|
||||||
constructor(private App: TemplatedApp) {
|
constructor(private App: TemplatedApp) {
|
||||||
@ -25,35 +27,45 @@ export class MapController extends BaseController {
|
|||||||
|
|
||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
if (typeof query.organizationSlug !== "string") {
|
if (typeof query.playUri !== "string") {
|
||||||
console.error("Expected organizationSlug parameter");
|
console.error("Expected playUri parameter in /map endpoint");
|
||||||
res.writeStatus("400 Bad request");
|
res.writeStatus("400 Bad request");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("Expected organizationSlug parameter");
|
res.end("Expected playUri parameter");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof query.worldSlug !== "string") {
|
|
||||||
console.error("Expected worldSlug parameter");
|
// If no admin URL is set, let's react on '/_/[instance]/[map url]' URLs
|
||||||
res.writeStatus("400 Bad request");
|
if (!ADMIN_API_URL) {
|
||||||
|
const roomUrl = new URL(query.playUri);
|
||||||
|
|
||||||
|
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname);
|
||||||
|
if (!match) {
|
||||||
|
res.writeStatus("404 Not Found");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
res.end(JSON.stringify({}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapUrl = roomUrl.protocol + "//" + match[1];
|
||||||
|
|
||||||
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end("Expected worldSlug parameter");
|
res.end(
|
||||||
return;
|
JSON.stringify({
|
||||||
}
|
mapUrl,
|
||||||
if (typeof query.roomSlug !== "string" && query.roomSlug !== undefined) {
|
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
|
||||||
console.error("Expected only one roomSlug parameter");
|
roomSlug: "", // Deprecated
|
||||||
res.writeStatus("400 Bad request");
|
tags: [],
|
||||||
this.addCorsHeaders(res);
|
} as MapDetailsData)
|
||||||
res.end("Expected only one roomSlug parameter");
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const mapDetails = await adminApi.fetchMapDetails(
|
const mapDetails = await adminApi.fetchMapDetails(query.playUri as string);
|
||||||
query.organizationSlug as string,
|
|
||||||
query.worldSlug as string,
|
|
||||||
query.roomSlug as string | undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
res.writeStatus("200 OK");
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
|
@ -1,42 +1,27 @@
|
|||||||
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface";
|
||||||
import { PositionDispatcher } from "./PositionDispatcher";
|
import { PositionDispatcher } from "./PositionDispatcher";
|
||||||
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
import { ViewportInterface } from "_Model/Websocket/ViewportMessage";
|
||||||
import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier";
|
|
||||||
import { arrayIntersect } from "../Services/ArrayHelper";
|
import { arrayIntersect } from "../Services/ArrayHelper";
|
||||||
import { ZoneEventListener } from "_Model/Zone";
|
import { ZoneEventListener } from "_Model/Zone";
|
||||||
|
|
||||||
export enum GameRoomPolicyTypes {
|
export enum GameRoomPolicyTypes {
|
||||||
ANONYMUS_POLICY = 1,
|
ANONYMOUS_POLICY = 1,
|
||||||
MEMBERS_ONLY_POLICY,
|
MEMBERS_ONLY_POLICY,
|
||||||
USE_TAGS_POLICY,
|
USE_TAGS_POLICY,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PusherRoom {
|
export class PusherRoom {
|
||||||
private readonly positionNotifier: PositionDispatcher;
|
private readonly positionNotifier: PositionDispatcher;
|
||||||
public readonly public: boolean;
|
|
||||||
public tags: string[];
|
public tags: string[];
|
||||||
public policyType: GameRoomPolicyTypes;
|
public policyType: GameRoomPolicyTypes;
|
||||||
public readonly roomSlug: string;
|
|
||||||
public readonly worldSlug: string = "";
|
|
||||||
public readonly organizationSlug: string = "";
|
|
||||||
private versionNumber: number = 1;
|
private versionNumber: number = 1;
|
||||||
|
|
||||||
constructor(public readonly roomId: string, private socketListener: ZoneEventListener) {
|
constructor(public readonly roomUrl: string, private socketListener: ZoneEventListener) {
|
||||||
this.public = isRoomAnonymous(roomId);
|
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY;
|
this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY;
|
||||||
|
|
||||||
if (this.public) {
|
|
||||||
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
|
||||||
} else {
|
|
||||||
const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId);
|
|
||||||
this.roomSlug = roomSlug;
|
|
||||||
this.organizationSlug = organizationSlug;
|
|
||||||
this.worldSlug = worldSlug;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A zone is 10 sprites wide.
|
// A zone is 10 sprites wide.
|
||||||
this.positionNotifier = new PositionDispatcher(this.roomId, 320, 320, this.socketListener);
|
this.positionNotifier = new PositionDispatcher(this.roomUrl, 320, 320, this.socketListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
|
public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void {
|
||||||
|
@ -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 };
|
|
||||||
};
|
|
@ -3,9 +3,7 @@ import Axios from "axios";
|
|||||||
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
|
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
|
||||||
|
|
||||||
export interface AdminApiData {
|
export interface AdminApiData {
|
||||||
organizationSlug: string;
|
roomUrl: string;
|
||||||
worldSlug: string;
|
|
||||||
roomSlug: string;
|
|
||||||
mapUrlStart: string;
|
mapUrlStart: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
policy_type: number;
|
policy_type: number;
|
||||||
@ -43,24 +41,15 @@ export interface FetchMemberDataByUuidResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AdminApi {
|
class AdminApi {
|
||||||
async fetchMapDetails(
|
async fetchMapDetails(playUri: string): Promise<MapDetailsData> {
|
||||||
organizationSlug: string,
|
|
||||||
worldSlug: string,
|
|
||||||
roomSlug: string | undefined
|
|
||||||
): Promise<MapDetailsData> {
|
|
||||||
if (!ADMIN_API_URL) {
|
if (!ADMIN_API_URL) {
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
return Promise.reject(new Error("No admin backoffice set!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const params: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
|
const params: { playUri: string } = {
|
||||||
organizationSlug,
|
playUri,
|
||||||
worldSlug,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (roomSlug) {
|
|
||||||
params.roomSlug = roomSlug;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
||||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||||
params,
|
params,
|
||||||
@ -121,26 +110,20 @@ class AdminApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyBanUser(
|
async verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string): Promise<AdminBannedData> {
|
||||||
organizationMemberToken: string,
|
|
||||||
ipAddress: string,
|
|
||||||
organization: string,
|
|
||||||
world: string
|
|
||||||
): Promise<AdminBannedData> {
|
|
||||||
if (!ADMIN_API_URL) {
|
if (!ADMIN_API_URL) {
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
return Promise.reject(new Error("No admin backoffice set!"));
|
||||||
}
|
}
|
||||||
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
|
||||||
return Axios.get(
|
return Axios.get(
|
||||||
ADMIN_API_URL +
|
ADMIN_API_URL +
|
||||||
"/api/check-moderate-user/" +
|
"/api/ban" +
|
||||||
organization +
|
|
||||||
"/" +
|
|
||||||
world +
|
|
||||||
"?ipAddress=" +
|
"?ipAddress=" +
|
||||||
ipAddress +
|
encodeURIComponent(ipAddress) +
|
||||||
"&token=" +
|
"&token=" +
|
||||||
organizationMemberToken,
|
encodeURIComponent(userUuid) +
|
||||||
|
"&roomUrl=" +
|
||||||
|
encodeURIComponent(roomUrl),
|
||||||
{ headers: { Authorization: `${ADMIN_API_TOKEN}` } }
|
{ headers: { Authorization: `${ADMIN_API_TOKEN}` } }
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
return data.data;
|
return data.data;
|
||||||
|
@ -9,7 +9,7 @@ class JWTTokenManager {
|
|||||||
return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token
|
return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserUuidFromToken(token: unknown, ipAddress?: string, room?: string): Promise<string> {
|
public async getUserUuidFromToken(token: unknown, ipAddress?: string, roomUrl?: string): Promise<string> {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("An authentication error happened, a user tried to connect without a token.");
|
throw new Error("An authentication error happened, a user tried to connect without a token.");
|
||||||
}
|
}
|
||||||
@ -50,8 +50,8 @@ class JWTTokenManager {
|
|||||||
if (ADMIN_API_URL) {
|
if (ADMIN_API_URL) {
|
||||||
//verify user in admin
|
//verify user in admin
|
||||||
let promise = new Promise((resolve) => resolve());
|
let promise = new Promise((resolve) => resolve());
|
||||||
if (ipAddress && room) {
|
if (ipAddress && roomUrl) {
|
||||||
promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, room);
|
promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, roomUrl);
|
||||||
}
|
}
|
||||||
promise
|
promise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -79,19 +79,9 @@ class JWTTokenManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private verifyBanUser(userUuid: string, ipAddress: string, room: string): Promise<AdminBannedData> {
|
private verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string): Promise<AdminBannedData> {
|
||||||
const parts = room.split("/");
|
|
||||||
if (parts.length < 3 || parts[0] !== "@") {
|
|
||||||
return Promise.resolve({
|
|
||||||
is_banned: false,
|
|
||||||
message: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = parts[1];
|
|
||||||
const world = parts[2];
|
|
||||||
return adminApi
|
return adminApi
|
||||||
.verifyBanUser(userUuid, ipAddress, organization, world)
|
.verifyBanUser(userUuid, ipAddress, roomUrl)
|
||||||
.then((data: AdminBannedData) => {
|
.then((data: AdminBannedData) => {
|
||||||
if (data && data.is_banned) {
|
if (data && data.is_banned) {
|
||||||
throw new Error("User was banned");
|
throw new Error("User was banned");
|
||||||
|
@ -32,7 +32,7 @@ import {
|
|||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import { JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_URL, JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable";
|
||||||
import { adminApi, CharacterTexture } from "./AdminApi";
|
import { adminApi, CharacterTexture } from "./AdminApi";
|
||||||
import { emitInBatch } from "./IoSocketHelpers";
|
import { emitInBatch } from "./IoSocketHelpers";
|
||||||
import Jwt from "jsonwebtoken";
|
import Jwt from "jsonwebtoken";
|
||||||
@ -358,23 +358,24 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateRoom(roomId: string): Promise<PusherRoom> {
|
async getOrCreateRoom(roomUrl: string): Promise<PusherRoom> {
|
||||||
//check and create new world for a room
|
//check and create new world for a room
|
||||||
let world = this.rooms.get(roomId);
|
let room = this.rooms.get(roomUrl);
|
||||||
if (world === undefined) {
|
if (room === undefined) {
|
||||||
world = new PusherRoom(roomId, this);
|
room = new PusherRoom(roomUrl, this);
|
||||||
if (!world.public) {
|
if (ADMIN_API_URL) {
|
||||||
await this.updateRoomWithAdminData(world);
|
await this.updateRoomWithAdminData(room);
|
||||||
}
|
}
|
||||||
this.rooms.set(roomId, world);
|
|
||||||
|
this.rooms.set(roomUrl, room);
|
||||||
}
|
}
|
||||||
return Promise.resolve(world);
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateRoomWithAdminData(world: PusherRoom): Promise<void> {
|
public async updateRoomWithAdminData(room: PusherRoom): Promise<void> {
|
||||||
const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug);
|
const data = await adminApi.fetchMapDetails(room.roomUrl);
|
||||||
world.tags = data.tags;
|
room.tags = data.tags;
|
||||||
world.policyType = Number(data.policy_type);
|
room.policyType = Number(data.policy_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
|
emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) {
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
})
|
|
Loading…
Reference in New Issue
Block a user