Merge pull request #1672 from thecodingmachine/2daysLimit

2 days limit
This commit is contained in:
David Négrier 2022-01-05 17:42:01 +01:00 committed by GitHub
commit c46882e099
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 271 additions and 56 deletions

View file

@ -1,15 +1,15 @@
// lib/server.ts // lib/server.ts
import App from "./src/App"; import App from "./src/App";
import grpc from "grpc"; import grpc from "grpc";
import {roomManager} from "./src/RoomManager"; import { roomManager } from "./src/RoomManager";
import {IRoomManagerServer, RoomManagerService} from "./src/Messages/generated/messages_grpc_pb"; import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb";
import {HTTP_PORT, GRPC_PORT} from "./src/Enum/EnvironmentVariable"; import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable";
App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT)) App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT));
const server = new grpc.Server(); const server = new grpc.Server();
server.addService<IRoomManagerServer>(RoomManagerService, roomManager); server.addService<IRoomManagerServer>(RoomManagerService, roomManager);
server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure()); server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure());
server.start(); server.start();
console.log('WorkAdventure HTTP/2 API starting on port %d!', GRPC_PORT); console.log("WorkAdventure HTTP/2 API starting on port %d!", GRPC_PORT);

View file

@ -23,6 +23,9 @@
import { chatVisibilityStore } from "../Stores/ChatStore"; import { chatVisibilityStore } from "../Stores/ChatStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore"; import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte"; import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore";
import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte"; import AudioPlaying from "./UI/AudioPlaying.svelte";
import { soundPlayingStore } from "../Stores/SoundPlayingStore"; import { soundPlayingStore } from "../Stores/SoundPlayingStore";
import ErrorDialog from "./UI/ErrorDialog.svelte"; import ErrorDialog from "./UI/ErrorDialog.svelte";
@ -136,6 +139,16 @@
<HelpCameraSettingsPopup /> <HelpCameraSettingsPopup />
</div> </div>
{/if} {/if}
{#if $showLimitRoomModalStore}
<div>
<LimitRoomModal />
</div>
{/if}
{#if $showShareLinkMapModalStore}
<div>
<ShareLinkMapModal />
</div>
{/if}
{#if $requestVisitCardsStore} {#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore} /> <VisitCard visitCardUrl={$requestVisitCardsStore} />
{/if} {/if}

View file

@ -21,12 +21,12 @@
<div class="guest-main"> <div class="guest-main">
<section class="container-overflow"> <section class="container-overflow">
<section class="share-url not-mobile"> <section class="share-url not-mobile">
<h3>Share the link of the room !</h3> <h3>Share the link of the room!</h3>
<input type="text" readonly id="input-share-link" value={location.toString()} /> <input type="text" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button> <button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
</section> </section>
<section class="is-mobile"> <section class="is-mobile">
<h3>Share the link of the room !</h3> <h3>Share the link of the room!</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} /> <input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button> <button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
</section> </section>

View file

@ -1,9 +1,14 @@
<script lang="typescript"> <script lang="typescript">
import logoTalk from "../images/logo-message-pixel.png"; import logoTalk from "../images/logo-message-pixel.png";
import logoWA from "../images/logo-WA-pixel.png"; import logoWA from "../images/logo-WA-pixel.png";
import logoInvite from "../images/logo-invite-pixel.png";
import logoRegister from "../images/logo-register-pixel.png";
import { menuVisiblilityStore } from "../../Stores/MenuStore"; import { menuVisiblilityStore } from "../../Stores/MenuStore";
import { chatVisibilityStore } from "../../Stores/ChatStore"; import { chatVisibilityStore } from "../../Stores/ChatStore";
import { limitMapStore } from "../../Stores/GameStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
function showMenu() { function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore)); menuVisiblilityStore.set(!get(menuVisiblilityStore));
@ -11,13 +16,25 @@
function showChat() { function showChat() {
chatVisibilityStore.set(true); chatVisibilityStore.set(true);
} }
function register() {
window.open(`${ADMIN_URL}/second-step-register`, "_self");
}
function showInvite() {
showShareLinkMapModalStore.set(true);
}
</script> </script>
<svelte:window /> <svelte:window />
<main class="menuIcon"> <main class="menuIcon">
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} /> {#if $limitMapStore}
<img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} /> <img src={logoInvite} alt="open menu" class="nes-pointer" on:click|preventDefault={showInvite} />
<img src={logoRegister} alt="open menu" class="nes-pointer" on:click|preventDefault={register} />
{:else}
<img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} />
<img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} />
{/if}
</main> </main>
<style lang="scss"> <style lang="scss">

View file

@ -0,0 +1,47 @@
<script lang="typescript">
import { fly } from "svelte/transition";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
function register() {
window.open(`${ADMIN_URL}/second-step-register`, "_self");
}
</script>
<div class="limit-map nes-container" transition:fly={{ y: -900, duration: 500 }}>
<section>
<h2>Limit of your room</h2>
<p>Register your account!</p>
<p>
This map is limited in the time and to continue to use WorkAdventure, you must register your account in our
back office.
</p>
</section>
<section>
<button class="nes-btn is-primary" on:click|preventDefault={register}>Register</button>
</section>
</div>
<style lang="scss">
.limit-map {
pointer-events: auto;
background: #eceeee;
margin-left: auto;
margin-right: auto;
margin-top: 10vh;
max-height: 80vh;
max-width: 80vw;
overflow: auto;
text-align: center;
h2 {
font-family: "Press Start 2P";
}
section {
p {
margin: 15px;
font-family: "Press Start 2P";
}
}
}
</style>

View file

@ -0,0 +1,90 @@
<script lang="typescript">
import { fly } from "svelte/transition";
import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
interface ExtNavigator extends Navigator {
canShare?(data?: ShareData): Promise<boolean>;
}
const myNavigator: ExtNavigator = window.navigator;
const haveNavigatorSharingFeature: boolean =
myNavigator && myNavigator.canShare != null && myNavigator.share != null;
let copied: boolean = false;
function copyLink() {
try {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
input.focus();
input.select();
document.execCommand("copy");
copied = true;
} catch (e) {
console.error(e);
copied = false;
}
}
async function shareLink() {
const shareData = { url: location.toString() };
try {
await myNavigator.share(shareData);
} catch (err) {
console.error("Error: " + err);
copyLink();
}
}
function close() {
showShareLinkMapModalStore.set(false);
copied = false;
}
</script>
<div class="share-link-map nes-container" transition:fly={{ y: -900, duration: 500 }}>
<section>
<h2>Invite your friends or colleagues</h2>
<p>Share the link of the room!</p>
</section>
<section>
{#if haveNavigatorSharingFeature}
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
{:else}
<input type="text" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
{/if}
{#if copied}
<p>Copied!</p>
{/if}
</section>
<section>
<button class="nes-btn" on:click|preventDefault={close}>Close</button>
</section>
</div>
<style lang="scss">
div.share-link-map {
pointer-events: auto;
background: #eceeee;
margin-left: auto;
margin-right: auto;
margin-top: 10vh;
max-height: 80vh;
max-width: 80vw;
overflow: auto;
text-align: center;
h2 {
font-family: "Press Start 2P";
}
section {
p {
margin: 15px;
font-family: "Press Start 2P";
}
}
}
</style>

View file

@ -6,12 +6,11 @@
import type { Unsubscriber } from "svelte/store"; import type { Unsubscriber } from "svelte/store";
import { playersStore } from "../../Stores/PlayersStore"; import { playersStore } from "../../Stores/PlayersStore";
import { connectionManager } from "../../Connexion/ConnectionManager"; import { connectionManager } from "../../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../../Url/UrlManager";
import { get } from "svelte/store"; import { get } from "svelte/store";
let blockActive = true; let blockActive = true;
let reportActive = !blockActive; let reportActive = !blockActive;
let anonymous: boolean = false; let disableReport: boolean = false;
let userUUID: string | undefined = playersStore.getPlayerById(get(showReportScreenStore).userId)?.userUuid; let userUUID: string | undefined = playersStore.getPlayerById(get(showReportScreenStore).userId)?.userUuid;
let userName = "No name"; let userName = "No name";
let unsubscriber: Unsubscriber; let unsubscriber: Unsubscriber;
@ -26,7 +25,7 @@
} }
} }
}); });
anonymous = connectionManager.getConnexionType === GameConnexionTypes.anonymous; disableReport = !connectionManager.currentRoom?.canReport ?? true;
}); });
onDestroy(() => { onDestroy(() => {
@ -65,7 +64,7 @@
<button type="button" class="nes-btn" on:click|preventDefault={close}>X</button> <button type="button" class="nes-btn" on:click|preventDefault={close}>X</button>
</section> </section>
</section> </section>
<section class="report-menu-action {anonymous ? 'hidden' : ''}"> <section class="report-menu-action {disableReport ? 'hidden' : ''}">
<section class="justify-center"> <section class="justify-center">
<button <button
type="button" type="button"

View file

@ -1,20 +1,26 @@
<script lang="typescript"> <script lang="typescript">
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import { userIsAdminStore } from "../../Stores/GameStore"; import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore";
import { ADMIN_URL } from "../../Enum/EnvironmentVariable"; import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
const upgradeLink = ADMIN_URL + "/pricing"; const upgradeLink = ADMIN_URL + "/pricing";
const registerLink = ADMIN_URL + "/second-step-register";
</script> </script>
<main class="warningMain" transition:fly={{ y: -200, duration: 500 }}> <main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
<h2>Warning!</h2>
{#if $userIsAdminStore} {#if $userIsAdminStore}
<h2>Warning!</h2>
<p> <p>
This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank" This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank"
>here</a >here</a
> >
</p> </p>
{:else if $limitMapStore}
<p>
This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>!
</p>
{:else} {:else}
<h2>Warning!</h2>
<p>This world is close to its limit!</p> <p>This world is close to its limit!</p>
{/if} {/if}
</main> </main>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

View file

@ -8,12 +8,14 @@ import { CharacterTexture, LocalUser } from "./LocalUser";
import { Room } from "./Room"; import { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker"; import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore"; import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore"; import { userIsConnected, warningContainerStore } from "../Stores/MenuStore";
import { analyticsClient } from "../Administration/AnalyticsClient"; import { analyticsClient } from "../Administration/AnalyticsClient";
import { axiosWithRetry } from "./AxiosUtils"; import { axiosWithRetry } from "./AxiosUtils";
import axios from "axios"; import axios from "axios";
import { isRegisterData } from "../Messages/JsonMessages/RegisterData"; import { isRegisterData } from "../Messages/JsonMessages/RegisterData";
import { isAdminApiData } from "../Messages/JsonMessages/AdminApiData"; import { isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
import { limitMapStore } from "../Stores/GameStore";
import { showLimitRoomModalStore } from "../Stores/ModalStore";
class ConnectionManager { class ConnectionManager {
private localUser!: LocalUser; private localUser!: LocalUser;
@ -152,11 +154,7 @@ class ConnectionManager {
) )
); );
urlManager.pushRoomIdToUrl(this._currentRoom); urlManager.pushRoomIdToUrl(this._currentRoom);
} else if ( } else if (connexionType === GameConnexionTypes.room || connexionType === GameConnexionTypes.empty) {
connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous ||
connexionType === GameConnexionTypes.empty
) {
this.authToken = localUserStore.getAuthToken(); this.authToken = localUserStore.getAuthToken();
let roomPath: string; let roomPath: string;
@ -237,6 +235,17 @@ class ConnectionManager {
analyticsClient.identifyUser(this.localUser.uuid, this.localUser.email); analyticsClient.identifyUser(this.localUser.uuid, this.localUser.email);
} }
//if limit room active test headband
if (this._currentRoom.expireOn !== undefined) {
warningContainerStore.activateWarningContainer();
limitMapStore.set(true);
//check time of map
if (new Date() > this._currentRoom.expireOn) {
showLimitRoomModalStore.set(true);
}
}
this.serviceWorker = new _ServiceWorker(); this.serviceWorker = new _ServiceWorker();
return Promise.resolve(this._currentRoom); return Promise.resolve(this._currentRoom);
} }

View file

@ -18,7 +18,10 @@ export interface RoomRedirect {
export class Room { export class Room {
public readonly id: string; public readonly id: string;
public readonly isPublic: boolean; /**
* @deprecated
*/
private readonly isPublic: boolean;
private _authenticationMandatory: boolean = DISABLE_ANONYMOUS; private _authenticationMandatory: boolean = DISABLE_ANONYMOUS;
private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER; private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
private _mapUrl: string | undefined; private _mapUrl: string | undefined;
@ -27,6 +30,8 @@ export class Room {
private readonly _search: URLSearchParams; private readonly _search: URLSearchParams;
private _contactPage: string | undefined; private _contactPage: string | undefined;
private _group: string | null = null; private _group: string | null = null;
private _expireOn: Date | undefined;
private _canReport: boolean = false;
private constructor(private roomUrl: URL) { private constructor(private roomUrl: URL) {
this.id = roomUrl.pathname; this.id = roomUrl.pathname;
@ -34,7 +39,7 @@ export class Room {
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.id.startsWith("*/")) {
this.isPublic = true; this.isPublic = true;
} else if (this.id.startsWith("@/")) { } else if (this.id.startsWith("@/")) {
this.isPublic = false; this.isPublic = false;
@ -121,6 +126,10 @@ export class Room {
data.authenticationMandatory != null ? data.authenticationMandatory : DISABLE_ANONYMOUS; data.authenticationMandatory != null ? data.authenticationMandatory : DISABLE_ANONYMOUS;
this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER; this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER;
this._contactPage = data.contactPage || CONTACT_URL; this._contactPage = data.contactPage || CONTACT_URL;
if (data.expireOn) {
this._expireOn = new Date(data.expireOn);
}
this._canReport = data.canReport ?? false;
return new MapDetail(data.mapUrl, data.textures); return new MapDetail(data.mapUrl, data.textures);
} else { } else {
throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format."); throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format.");
@ -143,6 +152,8 @@ export class Room {
* Instance name is: * Instance name is:
* - In a public URL: the second part of the URL ( _/[instance]/map.json) * - In a public URL: the second part of the URL ( _/[instance]/map.json)
* - In a private URL: [organizationId/worldId] * - In a private URL: [organizationId/worldId]
*
* @deprecated
*/ */
public getInstance(): string { public getInstance(): string {
if (this.instance !== undefined) { if (this.instance !== undefined) {
@ -150,7 +161,7 @@ export class Room {
} }
if (this.isPublic) { if (this.isPublic) {
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]; this.instance = match[1];
return this.instance; return this.instance;
@ -222,4 +233,12 @@ export class Room {
get group(): string | null { get group(): string | null {
return this._group; return this._group;
} }
get expireOn(): Date | undefined {
return this._expireOn;
}
get canReport(): boolean {
return this._canReport;
}
} }

View file

@ -5,3 +5,5 @@ export const userMovingStore = writable(false);
export const requestVisitCardsStore = writable<string | null>(null); export const requestVisitCardsStore = writable<string | null>(null);
export const userIsAdminStore = writable(false); export const userIsAdminStore = writable(false);
export const limitMapStore = writable(false);

View file

@ -0,0 +1,4 @@
import { writable } from "svelte/store";
export const showLimitRoomModalStore = writable(false);
export const showShareLinkMapModalStore = writable(false);

View file

@ -2,8 +2,7 @@ import type { Room } from "../Connexion/Room";
import { localUserStore } from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
export enum GameConnexionTypes { export enum GameConnexionTypes {
anonymous = 1, room = 1,
organization,
register, register,
empty, empty,
unknown, unknown,
@ -19,10 +18,8 @@ class UrlManager {
return GameConnexionTypes.login; return GameConnexionTypes.login;
} else if (url === "/jwt") { } else if (url === "/jwt") {
return GameConnexionTypes.jwt; return GameConnexionTypes.jwt;
} else if (url.includes("_/")) { } else if (url.includes("_/") || url.includes("*/") || url.includes("@/")) {
return GameConnexionTypes.anonymous; return GameConnexionTypes.room;
} else if (url.includes("@/")) {
return GameConnexionTypes.organization;
} else if (url.includes("register/")) { } else if (url.includes("register/")) {
return GameConnexionTypes.register; return GameConnexionTypes.register;
} else if (url === "/") { } else if (url === "/") {

View file

@ -1,22 +1,24 @@
import "jasmine"; import "jasmine";
import {PlayerMovement} from "../../../src/Phaser/Game/PlayerMovement"; import { PlayerMovement } from "../../../src/Phaser/Game/PlayerMovement";
describe("Interpolation / Extrapolation", () => { describe("Interpolation / Extrapolation", () => {
it("should interpolate", () => { it("should interpolate", () => {
const playerMovement = new PlayerMovement({ const playerMovement = new PlayerMovement(
x: 100, y: 200 {
}, 42000, x: 100,
y: 200,
},
42000,
{ {
x: 200, x: 200,
y: 100, y: 100,
oldX: 100, oldX: 100,
oldY: 200, oldY: 200,
moving: true, moving: true,
direction: "up" direction: "up",
}, },
42200 42200
); );
expect(playerMovement.isOutdated(42100)).toBe(false); expect(playerMovement.isOutdated(42100)).toBe(false);
expect(playerMovement.isOutdated(43000)).toBe(true); expect(playerMovement.isOutdated(43000)).toBe(true);
@ -26,8 +28,8 @@ describe("Interpolation / Extrapolation", () => {
y: 150, y: 150,
oldX: 100, oldX: 100,
oldY: 200, oldY: 200,
direction: 'up', direction: "up",
moving: true moving: true,
}); });
expect(playerMovement.getPosition(42200)).toEqual({ expect(playerMovement.getPosition(42200)).toEqual({
@ -35,8 +37,8 @@ describe("Interpolation / Extrapolation", () => {
y: 100, y: 100,
oldX: 100, oldX: 100,
oldY: 200, oldY: 200,
direction: 'up', direction: "up",
moving: true moving: true,
}); });
expect(playerMovement.getPosition(42300)).toEqual({ expect(playerMovement.getPosition(42300)).toEqual({
@ -44,22 +46,25 @@ describe("Interpolation / Extrapolation", () => {
y: 50, y: 50,
oldX: 100, oldX: 100,
oldY: 200, oldY: 200,
direction: 'up', direction: "up",
moving: true moving: true,
}); });
}); });
it("should not extrapolate if we stop", () => { it("should not extrapolate if we stop", () => {
const playerMovement = new PlayerMovement({ const playerMovement = new PlayerMovement(
x: 100, y: 200 {
}, 42000, x: 100,
y: 200,
},
42000,
{ {
x: 200, x: 200,
y: 100, y: 100,
oldX: 100, oldX: 100,
oldY: 200, oldY: 200,
moving: false, moving: false,
direction: "up" direction: "up",
}, },
42200 42200
); );
@ -69,22 +74,25 @@ describe("Interpolation / Extrapolation", () => {
y: 100, y: 100,
oldX: 100, oldX: 100,
oldY: 200, oldY: 200,
direction: 'up', direction: "up",
moving: false moving: false,
}); });
}); });
it("should keep moving until it stops", () => { it("should keep moving until it stops", () => {
const playerMovement = new PlayerMovement({ const playerMovement = new PlayerMovement(
x: 100, y: 200 {
}, 42000, x: 100,
y: 200,
},
42000,
{ {
x: 200, x: 200,
y: 100, y: 100,
oldX: 100, oldX: 100,
oldY: 200, oldY: 200,
moving: false, moving: false,
direction: "up" direction: "up",
}, },
42200 42200
); );
@ -94,8 +102,8 @@ describe("Interpolation / Extrapolation", () => {
y: 150, y: 150,
oldX: 100, oldX: 100,
oldY: 200, oldY: 200,
direction: 'up', direction: "up",
moving: false moving: false,
}); });
}); });
}) });

View file

@ -20,6 +20,10 @@ export const isMapDetailsData = new tg.IsInterface()
}) })
.withOptionalProperties({ .withOptionalProperties({
iframeAuthentication: tg.isNullable(tg.isString), iframeAuthentication: tg.isNullable(tg.isString),
// The date (in ISO 8601 format) at which the room will expire
expireOn: tg.isString,
// Whether the "report" feature is enabled or not on this room
canReport: tg.isBoolean,
}) })
.get(); .get();