c9fa9b9a92
The notion of public/private repositories (with /_/ and /@/ URLs) is specific to the SAAS version of WorkAdventure. It would be better to avoid leaking the organization/world/room structure of the private SAAS URLs inside the WorkAdventure Github project. Rather than sending http://admin_host/api/map?organizationSlug=...&worldSlug=...&roomSlug=...., we are now sending /api/map&playUri=... where playUri is the full URL of the current game. This allows the backend to act as a complete router. The front (and the pusher) will be able to completely ignore the specifics of URL building (with /@/ and /_/ URLs, etc...) Those details will live only in the admin server, which is way cleaner (and way more powerful).
225 lines
7.2 KiB
TypeScript
225 lines
7.2 KiB
TypeScript
import Axios from "axios";
|
|
import { PUSHER_URL } from "../Enum/EnvironmentVariable";
|
|
import type { CharacterTexture } from "./LocalUser";
|
|
|
|
export class MapDetail {
|
|
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
|
|
}
|
|
|
|
export interface RoomRedirect {
|
|
redirectUrl: string;
|
|
}
|
|
|
|
export class Room {
|
|
public readonly id: string;
|
|
public readonly isPublic: boolean;
|
|
private _mapUrl: string | undefined;
|
|
private _textures: CharacterTexture[] | undefined;
|
|
private instance: string | undefined;
|
|
private readonly _search: URLSearchParams;
|
|
|
|
private constructor(private roomUrl: URL) {
|
|
this.id = roomUrl.pathname;
|
|
|
|
if (this.id.startsWith("/")) {
|
|
this.id = this.id.substr(1);
|
|
}
|
|
if (this.id.startsWith("_/")) {
|
|
this.isPublic = true;
|
|
} else if (this.id.startsWith("@/")) {
|
|
this.isPublic = false;
|
|
} else {
|
|
throw new Error("Invalid room ID");
|
|
}
|
|
|
|
this._search = new URLSearchParams(roomUrl.search);
|
|
}
|
|
|
|
/**
|
|
* Creates a "Room" object representing the room.
|
|
* This method will follow room redirects if necessary, so the instance returned is a "real" room.
|
|
*/
|
|
public static async createRoom(roomUrl: URL): Promise<Room> {
|
|
let redirectCount = 0;
|
|
while (redirectCount < 32) {
|
|
const room = new Room(roomUrl);
|
|
const result = await room.getMapDetail();
|
|
if (result instanceof MapDetail) {
|
|
return room;
|
|
}
|
|
redirectCount++;
|
|
roomUrl = new URL(result.redirectUrl);
|
|
}
|
|
throw new Error("Room resolving seems stuck in a redirect loop after 32 redirect attempts");
|
|
}
|
|
|
|
public static getRoomPathFromExitUrl(exitUrl: string, currentRoomUrl: string): URL {
|
|
const url = new URL(exitUrl, currentRoomUrl);
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* @deprecated USage of exitSceneUrl is deprecated and therefore, this method is deprecated too.
|
|
*/
|
|
public static getRoomPathFromExitSceneUrl(
|
|
exitSceneUrl: string,
|
|
currentRoomUrl: string,
|
|
currentMapUrl: string
|
|
): URL {
|
|
const absoluteExitSceneUrl = new URL(exitSceneUrl, currentMapUrl);
|
|
const baseUrl = new URL(currentRoomUrl);
|
|
|
|
const currentRoom = new Room(baseUrl);
|
|
let instance: string = "global";
|
|
if (currentRoom.isPublic) {
|
|
instance = currentRoom.instance as string;
|
|
}
|
|
|
|
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
|
|
if (absoluteExitSceneUrl.hash) {
|
|
baseUrl.hash = absoluteExitSceneUrl.hash;
|
|
}
|
|
|
|
return baseUrl;
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
public static getIdFromIdentifier(
|
|
identifier: string,
|
|
baseUrl: string,
|
|
currentInstance: string
|
|
): { roomId: string; hash: string | null } {
|
|
let roomId = "";
|
|
let hash = null;
|
|
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.
|
|
//We instead use 'workadventure' as a dummy base value.
|
|
const baseUrlObject = new URL(baseUrl);
|
|
const absoluteExitSceneUrl = new URL(
|
|
identifier,
|
|
"http://workadventure/_/" + currentInstance + "/" + baseUrlObject.hostname + baseUrlObject.pathname
|
|
);
|
|
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
|
|
roomId = roomId.substring(1); //remove the leading slash
|
|
hash = absoluteExitSceneUrl.hash;
|
|
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];
|
|
}
|
|
}
|
|
return { roomId, hash };
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Instance name is:
|
|
* - In a public URL: the second part of the URL ( _/[instance]/map.json)
|
|
* - In a private URL: [organizationId/worldId]
|
|
*/
|
|
public getInstance(): string {
|
|
if (this.instance !== undefined) {
|
|
return this.instance;
|
|
}
|
|
|
|
if (this.isPublic) {
|
|
const match = /_\/([^/]+)\/.+/.exec(this.id);
|
|
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
|
|
this.instance = match[1];
|
|
return this.instance;
|
|
} else {
|
|
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
|
|
if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
|
|
this.instance = match[1] + "/" + match[2];
|
|
return this.instance;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
private parsePrivateUrl(url: string): { organizationSlug: string; worldSlug: string; roomSlug?: string } {
|
|
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
|
|
const match = regex.exec(url);
|
|
if (!match) {
|
|
throw new Error("Invalid URL " + url);
|
|
}
|
|
const results: { organizationSlug: string; worldSlug: string; roomSlug?: string } = {
|
|
organizationSlug: match[1],
|
|
worldSlug: match[2],
|
|
};
|
|
if (match[3] !== undefined) {
|
|
results.roomSlug = match[3];
|
|
}
|
|
return results;
|
|
}
|
|
|
|
public isDisconnected(): boolean {
|
|
const alone = this._search.get("alone");
|
|
if (alone && alone !== "0" && alone.toLowerCase() !== "false") {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public get search(): URLSearchParams {
|
|
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;
|
|
}
|
|
}
|