diff --git a/back/package.json b/back/package.json index 1216efcf..eee46b56 100644 --- a/back/package.json +++ b/back/package.json @@ -40,6 +40,7 @@ }, "homepage": "https://github.com/thecodingmachine/workadventure#readme", "dependencies": { + "@anatine/zod-openapi": "^1.3.0", "@workadventure/tiled-map-type-guard": "^1.0.3", "axios": "^0.21.2", "busboy": "^0.3.1", @@ -50,6 +51,7 @@ "ipaddr.js": "^2.0.1", "jsonwebtoken": "^8.5.1", "mkdirp": "^1.0.4", + "openapi3-ts": "^2.0.2", "prom-client": "^12.0.0", "query-string": "^6.13.3", "redis": "^3.1.2", diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index efe7f86a..e2603ca8 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -520,7 +520,16 @@ export class GameRoom { this.admins.delete(admin); } - public incrementVersion(): number { + public async incrementVersion(): Promise { + // Let's check if the mapUrl has changed + const mapDetails = await GameRoom.getMapDetails(this.roomUrl); + if (this.mapUrl !== mapDetails.mapUrl) { + this.mapUrl = mapDetails.mapUrl; + this.mapPromise = undefined; + // Reset the variable manager + this.variableManagerPromise = undefined; + } + this.versionNumber++; return this.versionNumber; } diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index b103f240..d9ed323f 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -150,6 +150,9 @@ export class User implements Movable { if (this.outlineColor !== undefined) { playerDetails.setOutlinecolor(new UInt32Value().setValue(this.outlineColor)); } + if (details.getRemoveoutlinecolor()) { + playerDetails.setRemoveoutlinecolor(new BoolValue().setValue(true)); + } if (this.voiceIndicatorShown !== undefined) { playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown)); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 8d71f4d8..5af1096f 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -852,14 +852,14 @@ export class SocketManager { return; } - const versionNumber = room.incrementVersion(); + const versionNumber = await room.incrementVersion(); room.getUsers().forEach((recipient) => { - const worldFullMessage = new RefreshRoomMessage(); - worldFullMessage.setRoomid(roomId); - worldFullMessage.setVersionnumber(versionNumber); + const refreshRoomMessage = new RefreshRoomMessage(); + refreshRoomMessage.setRoomid(roomId); + refreshRoomMessage.setVersionnumber(versionNumber); const clientMessage = new ServerToClientMessage(); - clientMessage.setRefreshroommessage(worldFullMessage); + clientMessage.setRefreshroommessage(refreshRoomMessage); recipient.socket.write(clientMessage); }); diff --git a/back/yarn.lock b/back/yarn.lock index d23fc29b..615d7794 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@anatine/zod-openapi@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@anatine/zod-openapi/-/zod-openapi-1.3.0.tgz#b5b38c3d821b79674226aa7b327c88c371860d0d" + integrity sha512-l54DypUdDsIq1Uwjv4ib9IBkTXMKZQLUj7qvdFL51EExC5LdSSqOlTOyaVVZZGYgWPKM7ZjGklhdoknLz4EC+w== + dependencies: + ts-deepmerge "^1.1.0" + validator "^13.7.0" + "@babel/code-frame@^7.0.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" @@ -1563,6 +1571,13 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +openapi3-ts@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-2.0.2.tgz#a200dd838bf24c9086c8eedcfeb380b7eb31e82a" + integrity sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw== + dependencies: + yaml "^1.10.2" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -2042,6 +2057,11 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== +ts-deepmerge@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-1.1.0.tgz#4236ae102199affe2e77690dcf198a420160eef2" + integrity sha512-VvwaV/6RyYMwT9d8dClmfHIsG2PCdm6WY430QKOIbPRR50Y/1Q2ilp4i2XEZeHFcNqfaYnAQzpyUC6XA0AqqBg== + ts-node-dev@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.8.tgz#95520d8ab9d45fffa854d6668e2f8f9286241066" @@ -2153,6 +2173,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -2236,7 +2261,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: +yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== diff --git a/docs/maps/menu.php b/docs/maps/menu.php index c8afc2c0..af8acb9f 100644 --- a/docs/maps/menu.php +++ b/docs/maps/menu.php @@ -166,7 +166,8 @@ return [ ], [ 'title' => 'Troubleshooting', - 'url' => '/map-building/troubleshooting', - 'view' => 'content.map.troubleshooting' + 'url' => '/map-building/troubleshooting.md', + 'markdown' => 'maps.troubleshooting', + 'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/troubleshooting.md', ], ]; diff --git a/docs/maps/opening-a-website.md b/docs/maps/opening-a-website.md index 52a68168..47ed1a86 100644 --- a/docs/maps/opening-a-website.md +++ b/docs/maps/opening-a-website.md @@ -18,11 +18,18 @@ In order to create a zone that opens websites: {.alert.alert-warning} A website can explicitly forbid another website from loading it in an iFrame using -the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). +the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). You can +read more about this common issue and possible workaround the [troubleshooting guide](troubleshooting.md#embedding-an-iframe-is-forbidden). {.alert.alert-info} As an alternative, you may also put the `openWebsite` properties on a layer (rather than putting them on an "area" object) -but we advise to stick with "area" objects for better performance! +but we advise sticking with "area" objects for better performance! + +{.alert.alert-warning} +If the website you are embedding is using cookies, those cookies must be configured with the `SameSite=none` attribute. Otherwise, +they will be ignored by the browser. If you manage to see the website you embed but cannot log into it, the `SameSite` attribute is most +likely the culprit. You can read more about this common issue and possible workaround the [troubleshooting guide](troubleshooting.md#i-cannot-log-into-my-embedded-website). + ## Integrating a Youtube video diff --git a/docs/maps/troubleshooting.md b/docs/maps/troubleshooting.md new file mode 100644 index 00000000..784d1f66 --- /dev/null +++ b/docs/maps/troubleshooting.md @@ -0,0 +1,94 @@ +{.section-title.accent.text-primary} +# Troubleshooting + +## Look at the browser console + +If your map is not displayed correctly (most notably if you are getting a black screen), open your browser console. +This is usually done by pressing the F12 key and selecting the "console" tab. + +Scan the output. Towards the end, you might see a message explaining why your map cannot be loaded. + +## Check webserver CORS settings + +If you are hosting the map you built on your own webserver and if the map does not load, please check that +[your webserver CORS settings are correctly configured](hosting.md). + +## Issues embedding a website + +When you are embedding a website in WorkAdventure (whether it is using the [`openWebsite` property](opening-a-website.md) or +the [integrated website in a map](website-in-map.md) feature or the [Scripting API](scripting.md)), WorkAdventure +will open your website using an iFrame. + +Browsers have various security measures in place, and website owners can use those measures to prevent websites from +being used inside iFrames (either partially or completely). + +In the chapters below, we will list what can possibly prevent you from embedding a website, and see what are your options. + +### Embedding an iFrame is forbidden + +The worst that can happen is that the website you are trying to embed completely denies you the authorisation. +A website owner can do that using the [`X-Frame-Options` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options), +or the newer [`Content-Security-Policy` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). + +Take a look at the headers of the page you are trying to load. + +{.alert.alert-info} +You can view the headers of the web page you try to load in the developer tools of your browser (usually accessible using the F12 key +of your keyboard), in the network tab. Click on the top-most request and check the "Response Headers". + +Below is what you can see when opening a Youtube video page: + +![](images/x-frame-options.png) + +`X-Frame-Options: DENY` or `X-Frame-Options: SAMEORIGIN` will prevent WorkAdventure from loading the page. +`Content-Security-Policy` header have also the potential to prevent WorkAdventure from loading the page. + +If the website you are trying to embed has one of these headers set, here are your options: + +- if you have control over the website or know the owner, you can contact the owner/administrator of the website and ask for an exception +- otherwise, you can look for an "embed" option. Some websites have special pages that can be embedded. For instance, + YouTube has special "embed" links that can be used to embed a video in your website. A lot of websites have the same feature (you + can usually find those links in the "share" section) + +If none of these options are available to you, as a last resort, you can use the [`openTab` property](opening-a-website.md) instead of the `openWebsite` property. +It will open your webpage in another tab instead of opening it in an iFrame. + +### I cannot log into my embedded website + +When you log into a website, the website is issuing a "cookie". The cookie is a unique identifier that allows the website +to recognize you and to identify you. To improve the privacy of their users, browsers can sometimes treat cookies +inside iFrames as "third-party cookies" and discard them. + +Cookies can come with a `SameSite` attribute. + +The `SameSite` attribute can take these values: "Lax", "Strict" or "None". The only value that allows using the +cookie inside an iFrame is "None". + +{.alert.alert-info} +The `SameSite` attribute of your cookie MUST be set to "None" if you want to be able to use this cookie from an iFrame inside WorkAdventure. + +**Default values**: + +If the "SameSite" attribute is not explicitly set, [the behaviour depends on the browser](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#browser_compatibility). +Chrome, Edge and Opera will default to "Lax". +Firefox and Safari will default to "None" (as of 2022/04/25). + +As a result, a website that does not set the `SameSite` attribute on cookies will work correctly in Firefox and Safari but +login will fail on Chrome, Edge and Opera. + +If the website you are trying to embed has the `SameSite` attribute set to a value other than "None", here are your options: + +- if you have control over the website or know the owner, you can contact the owner/administrator of the website and ask + the owner/administrator to change the `SameSite` settings. +- otherwise, you will have to use the [`openTab` property](opening-a-website.md) instead of the `openWebsite` property. + It will open your webpage in another tab instead of in an iFrame. + +## Need some help? + +
+

WorkAdventure is a constantly evolving project and there is plenty of room for improvement regarding map editing.

+

If you are facing any troubles, do not hesitate to seek help in + our Discord server or open an "issue" in the + GitHub WorkAdventure account. +

+
diff --git a/front/.prettierignore b/front/.prettierignore index 8d8c68de..bef7f3b6 100644 --- a/front/.prettierignore +++ b/front/.prettierignore @@ -1,5 +1,3 @@ src/Messages/generated src/Messages/JsonMessages -src/i18n/i18n-svelte.ts -src/i18n/i18n-types.ts -src/i18n/i18n-util.ts +src/i18n/i18n-*.ts diff --git a/front/.typesafe-i18n.json b/front/.typesafe-i18n.json index 0cecbe32..9e01cf76 100644 --- a/front/.typesafe-i18n.json +++ b/front/.typesafe-i18n.json @@ -1,5 +1,5 @@ { - "$schema": "https://unpkg.com/typesafe-i18n@2.59.0/schema/typesafe-i18n.json", + "$schema": "https://unpkg.com/typesafe-i18n@5.4.0/schema/typesafe-i18n.json", "baseLocale": "en-US", "adapter": "svelte" } \ No newline at end of file diff --git a/front/package.json b/front/package.json index 1dd4cb3e..8d3b6dc7 100644 --- a/front/package.json +++ b/front/package.json @@ -5,7 +5,7 @@ "license": "SEE LICENSE IN LICENSE.txt", "devDependencies": { "@geprog/vite-plugin-env-config": "^4.0.3", - "@home-based-studio/phaser3-utils": "^0.4.2", + "@home-based-studio/phaser3-utils": "^0.4.7", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.36", "@tsconfig/svelte": "^1.0.10", "@types/google-protobuf": "^3.7.3", @@ -34,6 +34,7 @@ }, "dependencies": { "@16bits/nes.css": "^2.3.2", + "@anatine/zod-openapi": "^1.3.0", "@fontsource/press-start-2p": "^4.3.0", "@joeattardi/emoji-button": "^4.6.2", "@types/simple-peer": "^9.11.1", @@ -47,6 +48,7 @@ "easystarjs": "^0.4.4", "fast-deep-equal": "^3.1.3", "google-protobuf": "^3.13.0", + "openapi3-ts": "^2.0.2", "phaser": "3.55.1", "phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254", "phaser3-rex-plugins": "^1.1.42", @@ -61,7 +63,7 @@ "standardized-audio-context": "^25.2.4", "ts-deferred": "^1.0.4", "ts-proto": "^1.96.0", - "typesafe-i18n": "^2.59.0", + "typesafe-i18n": "^5.4.0", "uuidv4": "^6.2.10", "zod": "^3.14.3" }, diff --git a/front/src/Administration/AnalyticsClient.ts b/front/src/Administration/AnalyticsClient.ts index 8c14eaed..3c6bcb44 100644 --- a/front/src/Administration/AnalyticsClient.ts +++ b/front/src/Administration/AnalyticsClient.ts @@ -17,7 +17,7 @@ class AnalyticsClient { } } - identifyUser(uuid: string, email: string | null) { + identifyUser(uuid: string, email: string | null): void { this.posthogPromise ?.then((posthog) => { posthog.identify(uuid, { uuid, email, wa: true }); @@ -25,7 +25,7 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - loggedWithSso() { + loggedWithSso(): void { this.posthogPromise ?.then((posthog) => { posthog.capture("wa-logged-sso"); @@ -33,7 +33,7 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - loggedWithToken() { + loggedWithToken(): void { this.posthogPromise ?.then((posthog) => { posthog.capture("wa-logged-token"); @@ -41,7 +41,7 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - enteredRoom(roomId: string, roomGroup: string | null) { + enteredRoom(roomId: string, roomGroup: string | null): void { this.posthogPromise ?.then((posthog) => { posthog.capture("$pageView", { roomId, roomGroup }); @@ -50,7 +50,7 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - openedMenu() { + openedMenu(): void { this.posthogPromise ?.then((posthog) => { posthog.capture("wa-opened-menu"); @@ -58,7 +58,7 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - launchEmote(emote: string) { + launchEmote(emote: string): void { this.posthogPromise ?.then((posthog) => { posthog.capture("wa-emote-launch", { emote }); @@ -66,7 +66,7 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - enteredJitsi(roomName: string, roomId: string) { + enteredJitsi(roomName: string, roomId: string): void { this.posthogPromise ?.then((posthog) => { posthog.capture("wa-entered-jitsi", { roomName, roomId }); @@ -74,7 +74,7 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - validationName() { + validationName(): void { this.posthogPromise ?.then((posthog) => { posthog.capture("wa-name-validation"); @@ -82,7 +82,7 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - validationWoka(scene: string) { + validationWoka(scene: string): void { this.posthogPromise ?.then((posthog) => { posthog.capture("wa-woka-validation", { scene }); @@ -90,12 +90,309 @@ class AnalyticsClient { .catch((e) => console.error(e)); } - validationVideo() { + validationVideo(): void { this.posthogPromise ?.then((posthog) => { posthog.capture("wa-video-validation"); }) .catch((e) => console.error(e)); } + + /** New feature analytics **/ + openedChat(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-opened-chat"); + }) + .catch((e) => console.error(e)); + } + + openRegister(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-opened-register"); + }) + .catch((e) => console.error(e)); + } + + openInvite(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-opened-invite"); + }) + .catch((e) => console.error(e)); + } + + lockDiscussion(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_lockroom"); + }) + .catch((e) => console.error(e)); + } + + screenSharing(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa-screensharing"); + }) + .catch((e) => console.error(e)); + } + + follow(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_follow"); + }) + .catch((e) => console.error(e)); + } + + camera(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_camera"); + }) + .catch((e) => console.error(e)); + } + + microphone(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_microphone"); + }) + .catch((e) => console.error(e)); + } + + settingMicrophone(value: string): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_setting_microphone", { + checkbox: value, + }); + }) + .catch((e) => console.error(e)); + } + + settingCamera(value: string): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_setting_camera", { + checkbox: value, + }); + }) + .catch((e) => console.error(e)); + } + + settingNotification(value: string): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_setting_notification", { + checkbox: value, + }); + }) + .catch((e) => console.error(e)); + } + + settingFullscreen(value: string): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_setting_fullscreen", { + checkbox: value, + }); + }) + .catch((e) => console.error(e)); + } + + settingAskWebsite(value: string): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_setting_ask_website", { + checkbox: value, + }); + }) + .catch((e) => console.error(e)); + } + + settingRequestFollow(value: string): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_setting_request_follow", { + checkbox: value, + }); + }) + .catch((e) => console.error(e)); + } + + settingDecreaseAudioVolume(value: string): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_setting_decrease_audio_volume", { + checkbox: value, + }); + }) + .catch((e) => console.error(e)); + } + + login(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_login"); + }) + .catch((e) => console.error(e)); + } + + logout(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_logout"); + }) + .catch((e) => console.error(e)); + } + + switchMultiIframe(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_multiiframe_switch"); + }) + .catch((e) => console.error(e)); + } + + closeMultiIframe(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_multiiframe_close"); + }) + .catch((e) => console.error(e)); + } + + fullScreenMultiIframe(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_multiiframe_fullscreen"); + }) + .catch((e) => console.error(e)); + } + + stackOpenCloseMultiIframe(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_multiiframe_stack_open_close"); + }) + .catch((e) => console.error(e)); + } + + menuCredit(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_menu_credit"); + }) + .catch((e) => console.error(e)); + } + + menuProfile(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_menu_profile"); + }) + .catch((e) => console.error(e)); + } + + menuSetting() { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_menu_setting"); + }) + .catch((e) => console.error(e)); + } + + menuInvite(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_menu_invite"); + }) + .catch((e) => console.error(e)); + } + + globalMessage(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_menu_globalmessage"); + }) + .catch((e) => console.error(e)); + } + + menuContact(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_menu_contact"); + }) + .catch((e) => console.error(e)); + } + + inviteCopyLink(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_menu_invite_copylink"); + }) + .catch((e) => console.error(e)); + } + + inviteCopyLinkWalk(value: string): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_menu_invite_copylink_walk", { + checkbox: value, + }); + }) + .catch((e) => console.error(e)); + } + + editCompanion(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_edit_companion"); + }) + .catch((e) => console.error(e)); + } + + editCamera(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_edit_camera"); + }) + .catch((e) => console.error(e)); + } + + editName(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_edit_name"); + }) + .catch((e) => console.error(e)); + } + + editWoka(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_edit_woka"); + }) + .catch((e) => console.error(e)); + } + + selectWoka(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_wokascene_select"); + }) + .catch((e) => console.error(e)); + } + + selectCustomWoka(): void { + this.posthogPromise + ?.then((posthog) => { + posthog.capture("wa_wokascene_custom"); + }) + .catch((e) => console.error(e)); + } } export const analyticsClient = new AnalyticsClient(); diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 4ba156ae..4156b568 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -242,7 +242,7 @@ class IframeListener { } else if (iframeEvent.type === "cameraFollowPlayer") { this._cameraFollowPlayerStream.next(iframeEvent.data); } else if (iframeEvent.type === "chat") { - scriptUtils.sendAnonymousChat(iframeEvent.data); + scriptUtils.sendAnonymousChat(iframeEvent.data, iframe.contentWindow ?? undefined); } else if (iframeEvent.type === "openPopup") { this._openPopupStream.next(iframeEvent.data); } else if (iframeEvent.type === "closePopup") { @@ -404,13 +404,20 @@ class IframeListener { this.scripts.delete(scriptUrl); } - sendUserInputChat(message: string) { - this.postMessage({ - type: "userInputChat", - data: { - message: message, - } as UserInputChatEvent, - }); + /** + * @param message The message to dispatch + * @param exceptOrigin Don't dispatch the message to exceptOrigin (to avoid infinite loops) + */ + sendUserInputChat(message: string, exceptOrigin?: Window) { + this.postMessage( + { + type: "userInputChat", + data: { + message: message, + } as UserInputChatEvent, + }, + exceptOrigin + ); } sendEnterEvent(name: string) { @@ -528,8 +535,11 @@ class IframeListener { /** * Sends the message... to all allowed iframes. */ - public postMessage(message: IframeResponseEvent) { + public postMessage(message: IframeResponseEvent, exceptOrigin?: Window) { for (const iframe of this.iframes) { + if (exceptOrigin === iframe.contentWindow) { + continue; + } iframe.contentWindow?.postMessage(message, "*"); } } diff --git a/front/src/Api/ScriptUtils.ts b/front/src/Api/ScriptUtils.ts index f0a0625a..d6d70e81 100644 --- a/front/src/Api/ScriptUtils.ts +++ b/front/src/Api/ScriptUtils.ts @@ -11,9 +11,9 @@ class ScriptUtils { window.location.href = url; } - public sendAnonymousChat(chatEvent: ChatEvent) { + public sendAnonymousChat(chatEvent: ChatEvent, origin?: Window) { const userId = playersStore.addFacticePlayer(chatEvent.author); - chatMessagesStore.addExternalMessage(userId, chatEvent.message); + chatMessagesStore.addExternalMessage(userId, chatEvent.message, origin); } } diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index 623be040..af9e6fb5 100644 --- a/front/src/Components/CameraControls.svelte +++ b/front/src/Components/CameraControls.svelte @@ -17,6 +17,7 @@ import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore"; import { gameManager } from "../Phaser/Game/GameManager"; import { currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore"; + import { analyticsClient } from "../Administration/AnalyticsClient"; const gameScene = gameManager.getCurrentGameScene(); @@ -89,6 +90,7 @@ class="btn-follow nes-btn is-dark" class:hide={($peerStore.size === 0 && $followStateStore === "off") || $silentStore} class:disabled={$followStateStore !== "off"} + on:click={() => analyticsClient.follow()} on:click={followClick} > @@ -98,6 +100,7 @@ class="btn-lock nes-btn is-dark" class:hide={$peerStore.size === 0 || $silentStore} class:disabled={$currentPlayerGroupLockStateStore} + on:click={() => analyticsClient.lockDiscussion()} on:click={lockClick} > @@ -105,6 +108,7 @@
analyticsClient.screenSharing()} on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore || $silentStore} class:enabled={$requestedScreenSharingState} @@ -116,7 +120,12 @@ {/if}
-
+
analyticsClient.camera()} + on:click={cameraClick} + class:disabled={!$requestedCameraState || $silentStore} + > {#if $requestedCameraState && !$silentStore} Turn on webcam {:else} @@ -124,7 +133,12 @@ {/if}
-
+
analyticsClient.microphone()} + on:click={microphoneClick} + class:disabled={!$requestedMicrophoneState || $silentStore} + > {#if $requestedMicrophoneState && !$silentStore} Turn on microphone {:else} diff --git a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte index 63638873..940268d6 100644 --- a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte +++ b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte @@ -12,6 +12,7 @@ import { i18nJson } from "../../i18n/locales"; import uploadFile from "../images/jitsi.png"; + import { analyticsClient } from "../../Administration/AnalyticsClient"; export let index: number; export let coWebsite: CoWebsite; @@ -103,6 +104,7 @@ class:ready={$state === "ready"} class:displayed={isMain || isHighlight} class:vertical + on:click={() => analyticsClient.stackOpenCloseMultiIframe()} on:click={onClick} >
diff --git a/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte b/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte index d31aac89..95f3ef7c 100644 --- a/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte +++ b/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte @@ -19,6 +19,5 @@ padding-top: 2%; height: 100%; position: relative; - z-index: 200; } diff --git a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte index dbf7ee71..b483f6c4 100644 --- a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte +++ b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte @@ -67,10 +67,11 @@ {/key} {:else if $highlightedEmbedScreen.type === "cowebsite"} {#key $highlightedEmbedScreen.embed.getId()} -
+
+
+ import("./Video/DesktopCapturerSourcePicker.svelte")} + /> + {#if $menuVisiblilityStore} {/if} @@ -120,11 +125,6 @@ import("./EmoteMenu/EmoteMenu.svelte")} /> - import("./Video/DesktopCapturerSourcePicker.svelte")} - /> - {#if hasEmbedScreen} {/if} diff --git a/front/src/Components/Menu/GuestSubMenu.svelte b/front/src/Components/Menu/GuestSubMenu.svelte index 3afdfb51..5e37a665 100644 --- a/front/src/Components/Menu/GuestSubMenu.svelte +++ b/front/src/Components/Menu/GuestSubMenu.svelte @@ -2,6 +2,7 @@ import LL from "../../i18n/i18n-svelte"; import { gameManager } from "../../Phaser/Game/GameManager"; import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore"; + import { analyticsClient } from "../../Administration/AnalyticsClient"; let entryPoint: string = $startLayerNamesStore[0]; let walkAutomatically: boolean = false; @@ -54,16 +55,26 @@ class="link-url nes-input is-dark" value={location.toString()} /> - +
{:else}

{$LL.menu.invite.description()}

- +
{/if} -

Select an entry point

+

{$LL.menu.invite.selectEntryPoint()}

{#each displayableLocales as locale (locale.id)} - + {/each}
@@ -191,11 +192,21 @@
@@ -211,6 +222,7 @@ type="checkbox" class="nes-checkbox is-dark" bind:checked={fullscreen} + on:change={(event) => analyticsClient.settingFullscreen(event.currentTarget.value)} on:change={changeFullscreen} /> {$LL.menu.settings.fullscreen()} @@ -220,6 +232,7 @@ type="checkbox" class="nes-checkbox is-dark" bind:checked={notification} + on:change={(event) => analyticsClient.settingNotification(event.currentTarget.value)} on:change={changeNotification} /> {$LL.menu.settings.notifications()} @@ -229,6 +242,7 @@ type="checkbox" class="nes-checkbox is-dark" bind:checked={forceCowebsiteTrigger} + on:change={(event) => analyticsClient.settingAskWebsite(event.currentTarget.value)} on:change={changeForceCowebsiteTrigger} /> {$LL.menu.settings.cowebsiteTrigger()} @@ -238,6 +252,7 @@ type="checkbox" class="nes-checkbox is-dark" bind:checked={ignoreFollowRequests} + on:change={(event) => analyticsClient.settingRequestFollow(event.currentTarget.value)} on:change={changeIgnoreFollowRequests} /> {$LL.menu.settings.ignoreFollowRequest()} @@ -246,6 +261,7 @@ type="checkbox" class="nes-checkbox is-dark" bind:checked={decreaseAudioPlayerVolumeWhileTalking} + on:change={(event) => analyticsClient.settingDecreaseAudioVolume(event.currentTarget.value)} on:change={changeDecreaseAudioPlayerVolumeWhileTalking} /> {$LL.audio.manager.reduce()} diff --git a/front/src/Components/UI/ErrorScreen.svelte b/front/src/Components/UI/ErrorScreen.svelte index bac59594..11ab8ca4 100644 --- a/front/src/Components/UI/ErrorScreen.svelte +++ b/front/src/Components/UI/ErrorScreen.svelte @@ -2,6 +2,7 @@ import { fly } from "svelte/transition"; import { errorScreenStore } from "../../Stores/ErrorScreenStore"; import { gameManager } from "../../Phaser/Game/GameManager"; + import { connectionManager } from "../../Connexion/ConnectionManager"; import { get } from "svelte/store"; import { onDestroy } from "svelte"; @@ -11,7 +12,8 @@ let errorScreen = get(errorScreenStore); function click() { - window.location.reload(); + if (errorScreen.type === "unauthorized") void connectionManager.logout(); + else window.location.reload(); } let details = errorScreen.details; let timeVar = errorScreen.timeToRetry ?? 0; @@ -37,9 +39,9 @@

{detailsStylized}{#if $errorScreenStore.type === "retry"}

{/if}

- {#if $errorScreenStore.type === "retry" && $errorScreenStore.canRetryManual} + {#if ($errorScreenStore.type === "retry" && $errorScreenStore.canRetryManual) || $errorScreenStore.type === "unauthorized"} {/if} diff --git a/front/src/Components/selectCharacter/SelectCharacterScene.svelte b/front/src/Components/selectCharacter/SelectCharacterScene.svelte index ecf00828..d929d993 100644 --- a/front/src/Components/selectCharacter/SelectCharacterScene.svelte +++ b/front/src/Components/selectCharacter/SelectCharacterScene.svelte @@ -3,6 +3,7 @@ import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene"; import LL from "../../i18n/i18n-svelte"; import { customizeAvailableStore, selectedCollection } from "../../Stores/SelectCharacterSceneStore"; + import { analyticsClient } from "../../Administration/AnalyticsClient"; export let game: Game; @@ -40,13 +41,15 @@ analyticsClient.selectWoka()} + on:click={cameraScene}>{$LL.woka.selectWoka.continue()} {#if $customizeAvailableStore} analyticsClient.selectCustomWoka()} + on:click={customizeScene}>{$LL.woka.selectWoka.customize()} {/if} diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 50e564ff..9de89d93 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -364,15 +364,13 @@ class ConnectionManager { if (locale) { try { - if (locales.indexOf(locale) == -1) { - locales.forEach((l) => { - if (l.startsWith(locale.split("-")[0])) { - setCurrentLocale(l); - return; - } - }); + if (locales.indexOf(locale) !== -1) { + await setCurrentLocale(locale as Locales); } else { - setCurrentLocale(locale as Locales); + const nonRegionSpecificLocale = locales.find((l) => l.startsWith(locale.split("-")[0])); + if (nonRegionSpecificLocale) { + await setCurrentLocale(nonRegionSpecificLocale); + } } } catch (err) { console.warn("Could not set locale", err); diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 17725bdf..b220cd16 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -483,9 +483,15 @@ export class RoomConnection implements RoomConnection { } case "errorScreenMessage": { this._errorScreenMessageStream.next(message.errorScreenMessage); - if (message.errorScreenMessage.code !== "retry") this.closed = true; - console.error("An error occurred server side: " + message.errorScreenMessage.code); - errorScreenStore.setError(message.errorScreenMessage); + console.error("An error occurred server side: " + JSON.stringify(message.errorScreenMessage)); + if (message.errorScreenMessage.code !== "retry") { + this.closed = true; + } + if (message.errorScreenMessage.type === "redirect" && message.errorScreenMessage.urlToRedirect) { + window.location.assign(message.errorScreenMessage.urlToRedirect); + } else { + errorScreenStore.setError(message.errorScreenMessage); + } break; } default: { diff --git a/front/src/Phaser/Components/CustomizeWoka/CustomWokaPreviewer.ts b/front/src/Phaser/Components/CustomizeWoka/CustomWokaPreviewer.ts index 98836960..91f9c715 100644 --- a/front/src/Phaser/Components/CustomizeWoka/CustomWokaPreviewer.ts +++ b/front/src/Phaser/Components/CustomizeWoka/CustomWokaPreviewer.ts @@ -61,9 +61,8 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container { this.frame = this.scene.add.graphics(); this.turnIcon = this.scene.add .image(this.background.displayWidth * 0.35, this.background.displayHeight * 0.35, "iconTurn") - .setScale(0.25) - .setTintFill(0xffffff) - .setAlpha(0.5); + .setScale(0.2) + .setAlpha(0.75); this.drawFrame(); this.setSize(this.SIZE, this.SIZE); @@ -130,11 +129,11 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container { this.changeAnimation(direction, moving); this.turnIconTween?.stop(); - this.turnIcon.setScale(0.25); + this.turnIcon.setScale(0.2); this.turnIconTween = this.scene.tweens.add({ targets: [this.turnIcon], duration: 100, - scale: 0.2, + scale: 0.15, yoyo: true, ease: Easing.SineEaseIn, }); diff --git a/front/src/Phaser/Components/Ui/IconButton.ts b/front/src/Phaser/Components/Ui/IconButton.ts index 2fdc9cfd..61fa7ca5 100644 --- a/front/src/Phaser/Components/Ui/IconButton.ts +++ b/front/src/Phaser/Components/Ui/IconButton.ts @@ -6,6 +6,7 @@ export interface IconButtonConfig { hover: IconButtonAppearanceConfig; pressed: IconButtonAppearanceConfig; selected: IconButtonAppearanceConfig; + iconScale?: number; } export interface IconButtonAppearanceConfig { @@ -34,7 +35,7 @@ export class IconButton extends Phaser.GameObjects.Container { this.config = config; this.background = this.scene.add.graphics(); - this.icon = this.scene.add.image(0, 0, this.config.iconTextureKey); + this.icon = this.scene.add.image(0, 0, this.config.iconTextureKey).setScale(config.iconScale ?? 1); this.drawBackground(this.config.idle); this.add([this.background, this.icon]); diff --git a/front/src/Phaser/Entity/RemotePlayer.ts b/front/src/Phaser/Entity/RemotePlayer.ts index f820b578..ae0d1196 100644 --- a/front/src/Phaser/Entity/RemotePlayer.ts +++ b/front/src/Phaser/Entity/RemotePlayer.ts @@ -4,7 +4,7 @@ import { Character } from "../Entity/Character"; import type { GameScene } from "../Game/GameScene"; import type { PointInterface } from "../../Connexion/ConnexionModels"; import type { PlayerAnimationDirections } from "../Player/Animation"; -import type { Unsubscriber } from "svelte/store"; +import { get, Unsubscriber } from "svelte/store"; import type { ActivatableInterface } from "../Game/ActivatableInterface"; import type CancelablePromise from "cancelable-promise"; import LL from "../../i18n/i18n-svelte"; @@ -113,7 +113,7 @@ export class RemotePlayer extends Character implements ActivatableInterface { const actions: ActionsMenuAction[] = []; if (this.visitCardUrl) { actions.push({ - actionName: LL.woka.menu.businessCard(), + actionName: get(LL).woka.menu.businessCard(), protected: true, priority: 1, callback: () => { @@ -125,8 +125,8 @@ export class RemotePlayer extends Character implements ActivatableInterface { actions.push({ actionName: blackListManager.isBlackListed(this.userUuid) - ? LL.report.block.unblock() - : LL.report.block.block(), + ? get(LL).report.block.unblock() + : get(LL).report.block.block(), protected: true, priority: -1, style: "is-error", diff --git a/front/src/Phaser/Game/ActivatablesManager.ts b/front/src/Phaser/Game/ActivatablesManager.ts index b41c5a72..7f337cda 100644 --- a/front/src/Phaser/Game/ActivatablesManager.ts +++ b/front/src/Phaser/Game/ActivatablesManager.ts @@ -76,6 +76,7 @@ export class ActivatablesManager { const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition( this.directionalActivationPositionShift ); + this.activatableObjectsDistances.clear(); for (const object of objects) { const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition()); this.activatableObjectsDistances.set(object, distance); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 33cacaaf..6be4959c 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1135,6 +1135,13 @@ export class GameScene extends DirtyScene { }) ); + this.iframeSubscriptionList.push( + iframeListener.stopSoundStream.subscribe((stopSoundEvent) => { + const url = new URL(stopSoundEvent.url, this.MapUrlFile); + soundManager.stopSound(this.sound, url.toString()); + }) + ); + this.iframeSubscriptionList.push( iframeListener.addActionsMenuKeyToRemotePlayerStream.subscribe((data) => { this.MapPlayersByKey.get(data.id)?.registerActionsMenuAction({ diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts index 201c4db8..f07014b9 100644 --- a/front/src/Phaser/Login/CustomizeScene.ts +++ b/front/src/Phaser/Login/CustomizeScene.ts @@ -94,6 +94,7 @@ export class CustomizeScene extends AbstractCharacterScene { } public create(): void { + this.tryLoadLastUsedWokaLayers(); waScaleManager.zoomModifier = 1; this.createSlotBackgroundTextures(); this.initializeCustomWokaPreviewer(); @@ -149,12 +150,35 @@ export class CustomizeScene extends AbstractCharacterScene { this.scene.run(SelectCharacterSceneName); } + private tryLoadLastUsedWokaLayers(): void { + try { + const savedWokaLayers = gameManager.getCharacterLayers(); + if (savedWokaLayers && savedWokaLayers.length !== 0) { + this.selectedLayers = []; + for (let i = 0; i < savedWokaLayers.length; i += 1) { + this.selectedLayers.push( + this.layers[i].findIndex((item) => item.id === gameManager.getCharacterLayers()[i]) + ); + } + } + } catch { + console.warn("Cannot load previous WOKA"); + } + } + private createSlotBackgroundTextures(): void { for (let i = 0; i < 4; i += 1) { if (this.textures.getTextureKeys().includes(`floorTexture${i}`)) { continue; } - TexturesHelper.createFloorRectangleTexture(this, `floorTexture${i}`, 50, 50, "floorTiles", i); + TexturesHelper.createFloorRectangleTexture( + this, + `floorTexture${i}`, + WokaBodyPartSlot.SIZE, + WokaBodyPartSlot.SIZE, + "floorTiles", + i + ); } } @@ -213,15 +237,16 @@ export class CustomizeScene extends AbstractCharacterScene { ), [CustomWokaBodyPart.Body]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconBody")), [CustomWokaBodyPart.Clothes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconClothes")), - [CustomWokaBodyPart.Eyes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconEyes")), + [CustomWokaBodyPart.Eyes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconEyes", 0.7)), [CustomWokaBodyPart.Hair]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHair")), [CustomWokaBodyPart.Hat]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHat")), }; } - private getDefaultIconButtonConfig(iconTextureKey: string): IconButtonConfig { + private getDefaultIconButtonConfig(iconTextureKey: string, iconScale?: number): IconButtonConfig { return { iconTextureKey, + iconScale, width: 25, height: 25, idle: { @@ -327,13 +352,14 @@ export class CustomizeScene extends AbstractCharacterScene { } private handleCustomWokaPreviewerOnResize(): void { + const ratio = innerHeight / innerWidth; this.customWokaPreviewer.x = this.cameras.main.worldView.x + this.cameras.main.width / 2; - this.customWokaPreviewer.y = this.customWokaPreviewer.displayHeight * 0.5 + 10; + this.customWokaPreviewer.y = this.customWokaPreviewer.displayHeight * 0.5 + (ratio > 1.6 ? 40 : 10); } private handleBodyPartButtonsOnResize(): void { const ratio = innerHeight / innerWidth; - const slotDimension = 50; + const slotDimension = WokaBodyPartSlot.SIZE; for (const part in this.bodyPartsButtons) { this.bodyPartsButtons[part as CustomWokaBodyPart].setDisplaySize(slotDimension, slotDimension); @@ -420,7 +446,7 @@ export class CustomizeScene extends AbstractCharacterScene { private handleRandomizeButtonOnResize(): void { const x = - this.customWokaPreviewer.x + + this.customWokaPreviewer.x - (this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5; const y = this.customWokaPreviewer.y + @@ -431,7 +457,7 @@ export class CustomizeScene extends AbstractCharacterScene { private handleFinishButtonOnResize(): void { const x = - this.customWokaPreviewer.x - + this.customWokaPreviewer.x + (this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5; const y = this.customWokaPreviewer.y + diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts index dce7216d..68039403 100644 --- a/front/src/Phaser/Login/EntryScene.ts +++ b/front/src/Phaser/Login/EntryScene.ts @@ -6,7 +6,6 @@ import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene"; import { localeDetector } from "../../i18n/locales"; import { errorScreenStore } from "../../Stores/ErrorScreenStore"; import { isErrorApiData } from "../../Messages/JsonMessages/ErrorApiData"; -import { connectionManager } from "../../Connexion/ConnectionManager"; export const EntrySceneName = "EntryScene"; @@ -49,9 +48,7 @@ export class EntryScene extends Scene { .catch((err) => { const errorType = isErrorApiData.safeParse(err?.response?.data); if (errorType.success) { - if (errorType.data.type === "unauthorized") { - void connectionManager.logout(); - } else if (errorType.data.type === "redirect") { + if (errorType.data.type === "redirect") { window.location.assign(errorType.data.urlToRedirect); } else errorScreenStore.setError(err?.response?.data); } else { diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index 30323463..4f1316b0 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -18,6 +18,7 @@ import { DraggableGrid } from "@home-based-studio/phaser3-utils"; import { WokaSlot } from "../Components/SelectWoka/WokaSlot"; import { DraggableGridEvent } from "@home-based-studio/phaser3-utils/lib/utils/gui/containers/grids/DraggableGrid"; import { wokaList } from "../../Messages/JsonMessages/PlayerTextures"; +import { myCameraVisibilityStore } from "../../Stores/MyCameraStoreVisibility"; //todo: put this constants in a dedicated file export const SelectCharacterSceneName = "SelectCharacterScene"; @@ -133,6 +134,7 @@ export class SelectCharacterScene extends AbstractCharacterScene { return; } this.selectedWoka = null; + myCameraVisibilityStore.set(false); this.scene.sleep(SelectCharacterSceneName); this.scene.run(CustomizeSceneName); selectCharacterSceneVisibleStore.set(false); diff --git a/front/src/Stores/ChatStore.ts b/front/src/Stores/ChatStore.ts index b8d4ea7b..9dfe7e42 100644 --- a/front/src/Stores/ChatStore.ts +++ b/front/src/Stores/ChatStore.ts @@ -87,7 +87,10 @@ function createChatMessagesStore() { return list; }); }, - addExternalMessage(authorId: number, text: string) { + /** + * @param origin The iframe that originated this message (if triggered from the Scripting API), or undefined otherwise. + */ + addExternalMessage(authorId: number, text: string, origin?: Window) { update((list) => { const lastMessage = list[list.length - 1]; if ( @@ -106,7 +109,7 @@ function createChatMessagesStore() { }); } - iframeListener.sendUserInputChat(text); + iframeListener.sendUserInputChat(text, origin); return list; }); chatVisibilityStore.set(true); diff --git a/front/src/Stores/MenuStore.ts b/front/src/Stores/MenuStore.ts index b0ed4089..22d1e937 100644 --- a/front/src/Stores/MenuStore.ts +++ b/front/src/Stores/MenuStore.ts @@ -2,7 +2,6 @@ import { get, writable } from "svelte/store"; import Timeout = NodeJS.Timeout; import { userIsAdminStore } from "./GameStore"; import { CONTACT_URL, IDENTITY_URL, PROFILE_URL } from "../Enum/EnvironmentVariable"; -import { analyticsClient } from "../Administration/AnalyticsClient"; import type { Translation } from "../i18n/i18n-types"; import axios from "axios"; import { localUserStore } from "../Connexion/LocalUserStore"; @@ -14,7 +13,6 @@ export const userIsConnected = writable(false); export const profileAvailable = writable(true); menuVisiblilityStore.subscribe((value) => { - if (value) analyticsClient.openedMenu(); if (userIsConnected && value && IDENTITY_URL != null) { axios.get(getMeUrl()).catch((err) => { console.error("menuVisiblilityStore => err => ", err); diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 7bc53c23..b378b218 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -8,6 +8,7 @@ import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils"; import { LayoutMode } from "./LayoutManager"; import type { CoWebsite } from "./CoWebsite/CoWesbite"; import type CancelablePromise from "cancelable-promise"; +import { analyticsClient } from "../Administration/AnalyticsClient"; export enum iframeStates { closed = 1, @@ -126,6 +127,7 @@ class CoWebsiteManager { const buttonCloseCoWebsite = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId); buttonCloseCoWebsite.addEventListener("click", () => { + analyticsClient.closeMultiIframe(); const coWebsite = this.getMainCoWebsite(); if (!coWebsite) { @@ -143,6 +145,7 @@ class CoWebsiteManager { const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId); buttonFullScreenFrame.addEventListener("click", () => { + analyticsClient.fullScreenMultiIframe(); buttonFullScreenFrame.blur(); this.fullscreen(); }); @@ -159,6 +162,7 @@ class CoWebsiteManager { }); buttonSwipe.addEventListener("click", () => { + analyticsClient.switchMultiIframe(); const mainCoWebsite = this.getMainCoWebsite(); const highlightedEmbed = get(highlightedEmbedScreen); if (highlightedEmbed?.type === "cowebsite") { diff --git a/front/src/i18n/.gitignore b/front/src/i18n/.gitignore index 19f1f0f5..e2c9bddb 100644 --- a/front/src/i18n/.gitignore +++ b/front/src/i18n/.gitignore @@ -1,3 +1 @@ -i18n-svelte.ts -i18n-types.ts -i18n-util.ts \ No newline at end of file +i18n-*.ts diff --git a/front/src/i18n/de-DE/index.ts b/front/src/i18n/de-DE/index.ts index a72ecc1f..7c0dfb5b 100644 --- a/front/src/i18n/de-DE/index.ts +++ b/front/src/i18n/de-DE/index.ts @@ -16,8 +16,6 @@ import trigger from "./trigger"; const de_DE: Translation = { ...(en_US as Translation), - language: "Deutsch", - country: "Deutschland", audio, camera, chat, diff --git a/front/src/i18n/de-DE/menu.ts b/front/src/i18n/de-DE/menu.ts index f0dafeb8..f70922e2 100644 --- a/front/src/i18n/de-DE/menu.ts +++ b/front/src/i18n/de-DE/menu.ts @@ -77,7 +77,8 @@ const menu: NonNullable = { description: "Link zu diesem Raum teilen!", copy: "Kopieren", share: "Teilen", - walk_automatically_to_position: "Automatisch zu meiner Position gehen", + walkAutomaticallyToPosition: "Automatisch zu meiner Position gehen", + selectEntryPoint: "Select an entry point", }, globalMessage: { text: "Text", diff --git a/front/src/i18n/en-US/index.ts b/front/src/i18n/en-US/index.ts index b1a0e422..c0e018a2 100644 --- a/front/src/i18n/en-US/index.ts +++ b/front/src/i18n/en-US/index.ts @@ -14,8 +14,6 @@ import emoji from "./emoji"; import trigger from "./trigger"; const en_US: BaseTranslation = { - language: "English", - country: "United States", audio, camera, chat, diff --git a/front/src/i18n/en-US/menu.ts b/front/src/i18n/en-US/menu.ts index 928119d2..047d2bf4 100644 --- a/front/src/i18n/en-US/menu.ts +++ b/front/src/i18n/en-US/menu.ts @@ -77,7 +77,8 @@ const menu: BaseTranslation = { description: "Share the link of the room!", copy: "Copy", share: "Share", - walk_automatically_to_position: "Walk automatically to my position", + walkAutomaticallyToPosition: "Walk automatically to my position", + selectEntryPoint: "Select an entry point", }, globalMessage: { text: "Text", diff --git a/front/src/i18n/formatters.ts b/front/src/i18n/formatters.ts index 00695fd6..da775944 100644 --- a/front/src/i18n/formatters.ts +++ b/front/src/i18n/formatters.ts @@ -1,8 +1,8 @@ -import type { AsyncFormattersInitializer } from "typesafe-i18n"; +import type { FormattersInitializer } from "typesafe-i18n"; import type { Locales, Formatters } from "./i18n-types"; // eslint-disable-next-line @typescript-eslint/require-await -export const initFormatters: AsyncFormattersInitializer = async () => { +export const initFormatters: FormattersInitializer = async () => { const formatters: Formatters = { // add your formatter functions here }; diff --git a/front/src/i18n/locales.ts b/front/src/i18n/locales.ts index 538fc606..c18a1e5b 100644 --- a/front/src/i18n/locales.ts +++ b/front/src/i18n/locales.ts @@ -1,70 +1,44 @@ import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors"; import { FALLBACK_LOCALE } from "../Enum/EnvironmentVariable"; -import { initI18n, setLocale, locale } from "./i18n-svelte"; +import { setLocale } from "./i18n-svelte"; import type { Locales } from "./i18n-types"; -import { baseLocale, getTranslationForLocale, locales } from "./i18n-util"; -import { get } from "svelte/store"; +import { baseLocale, locales } from "./i18n-util"; +import { loadLocaleAsync } from "./i18n-util.async"; -const fallbackLocale = FALLBACK_LOCALE || baseLocale; +const fallbackLocale = (FALLBACK_LOCALE || baseLocale) as Locales; const localStorageProperty = "language"; export const localeDetector = async () => { const exist = localStorage.getItem(localStorageProperty); - let detectedLocale: Locales = fallbackLocale as Locales; + let detectedLocale: Locales = fallbackLocale; if (exist) { const localStorageDetector = initLocalStorageDetector(localStorageProperty); - detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector) as Locales; + detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector); } else { - detectedLocale = detectLocale(fallbackLocale, locales, navigatorDetector) as Locales; + detectedLocale = detectLocale(fallbackLocale, locales, navigatorDetector); } - await initI18n(detectedLocale); + await setCurrentLocale(detectedLocale); }; -export const setCurrentLocale = (locale: Locales) => { +export const setCurrentLocale = async (locale: Locales) => { localStorage.setItem(localStorageProperty, locale); - setLocale(locale).catch(() => { - console.log("Cannot reload the locale!"); - }); + await loadLocaleAsync(locale); + setLocale(locale); }; -export type DisplayableLocale = { id: Locales; language: string; country: string }; +export const displayableLocales: { id: Locales; language: string; region: string }[] = locales.map((locale) => { + const [language, region] = locale.split("-"); -function getDisplayableLocales() { - const localesObject: DisplayableLocale[] = []; - locales.forEach((locale) => { - getTranslationForLocale(locale) - .then((translations) => { - localesObject.push({ - id: locale, - language: translations.language, - country: translations.country, - }); - }) - .catch((error) => { - console.log(error); - }); - }); - - return localesObject; -} - -export const displayableLocales = getDisplayableLocales(); - -export const i18nJson = (text: string): string => { - if (text.trim().startsWith("{")) { - try { - const textObject = JSON.parse(text); - if (textObject[get(locale)]) { - return textObject[get(locale)]; - } else if (Object.keys(textObject).length > 0) { - // fallback to first value - return textObject[Object.keys(textObject)[0]]; - } - } catch (err) { - // - } + // backwards compatibility + if (!Intl.DisplayNames) { + return { id: locale, language, region }; } - return text; -}; + + return { + id: locale, + language: new Intl.DisplayNames(locale, { type: "language" }).of(language), + region: new Intl.DisplayNames(locale, { type: "region" }).of(region), + }; +}); diff --git a/front/style/cowebsite/_global.scss b/front/style/cowebsite/_global.scss index 202f0552..999ed54a 100644 --- a/front/style/cowebsite/_global.scss +++ b/front/style/cowebsite/_global.scss @@ -95,7 +95,7 @@ &-buffer { iframe { - z-index: 45 !important; + z-index: 201 !important; pointer-events: none !important; overflow: hidden; border: 0; diff --git a/front/tsconfig.json b/front/tsconfig.json index edb99eb5..e5ac9819 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -8,7 +8,7 @@ "moduleResolution": "node", //"module": "CommonJS", "module": "ESNext", - "target": "ES2017", + "target": "ES2020", "declaration": false, "downlevelIteration": true, "jsx": "react", diff --git a/front/yarn.lock b/front/yarn.lock index 46e8ced7..a5b24ce7 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -7,6 +7,14 @@ resolved "https://registry.yarnpkg.com/@16bits/nes.css/-/nes.css-2.3.2.tgz#e69db834119b33ae8d3cb044f106a07a17cadd6f" integrity sha512-nEM5PIth+Bab5JSOa4uUR+PMNUsNTYxA55oVlG3gXI/4LoYtWS767Uv9Pu/KCbHXVvnIjt4ZXt13kZw3083qTw== +"@anatine/zod-openapi@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@anatine/zod-openapi/-/zod-openapi-1.3.0.tgz#b5b38c3d821b79674226aa7b327c88c371860d0d" + integrity sha512-l54DypUdDsIq1Uwjv4ib9IBkTXMKZQLUj7qvdFL51EExC5LdSSqOlTOyaVVZZGYgWPKM7ZjGklhdoknLz4EC+w== + dependencies: + ts-deepmerge "^1.1.0" + validator "^13.7.0" + "@babel/runtime@^7.14.0": version "7.14.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" @@ -77,10 +85,10 @@ resolved "https://registry.yarnpkg.com/@geprog/vite-plugin-env-config/-/vite-plugin-env-config-4.0.3.tgz#ca04bd9ad9f55fe568917db79266afe8e766e25e" integrity sha512-2HDCV+6XXJjSuBAhDWLRr111buMQ3bIZrKo3dymIhEJ4oJCC/3yDqg7HDQIn8Y8KKbsM0AtuHMZW4yz2tPBsYg== -"@home-based-studio/phaser3-utils@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.2.tgz#b2c1815a6b51321ea8dab027b5badcf714d99fd6" - integrity sha512-S0VkAq3z0Kf0vEUUyCDes911icvc+nkUq7lGp23zD/5lk7LTGM51NswSAfel7Rm/DLY8IBxvDTBJADTf/De82w== +"@home-based-studio/phaser3-utils@^0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.7.tgz#d0464c81cb27328657d3fd048396f6936e200c48" + integrity sha512-gYt1mkuad85uzYwHK0+wp+mrsGASV4sRZPaHZHnO8A2ofTAnX36S3PcI+BqKchdJ0I7jvBQcfh0yp1Ug0BHT+A== dependencies: phaser "3.55.1" @@ -2060,6 +2068,13 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +openapi3-ts@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-2.0.2.tgz#a200dd838bf24c9086c8eedcfeb380b7eb31e82a" + integrity sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw== + dependencies: + yaml "^1.10.2" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -2880,6 +2895,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +ts-deepmerge@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-1.1.0.tgz#4236ae102199affe2e77690dcf198a420160eef2" + integrity sha512-VvwaV/6RyYMwT9d8dClmfHIsG2PCdm6WY430QKOIbPRR50Y/1Q2ilp4i2XEZeHFcNqfaYnAQzpyUC6XA0AqqBg== + ts-deferred@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/ts-deferred/-/ts-deferred-1.0.4.tgz#58145ebaeef5b8f2a290b8cec3d060839f9489c7" @@ -2996,10 +3016,10 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typesafe-i18n@^2.59.0: - version "2.59.0" - resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-2.59.0.tgz#09a9a32e61711418d927a389fa52e1c06a5fa5c4" - integrity sha512-Qv3Mrwmb8b73VNzQDPHPECzwymdBRVyDiZ3w2qnp4c2iv/7TGuiJegNHT/l3MooEN7IPbSpc5tbXw2x3MbGtFg== +typesafe-i18n@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-5.4.0.tgz#cab696160bb144c387d7cbd13f7a728aa8371777" + integrity sha512-htewpld3FzZQv3Y1G31w54bofaaKR11MCkDK0FIYuXCpX72y1G6fkXUDslqzZCyVkZWRnIhY8leviNDxLwEzRw== typescript@*: version "4.3.2" @@ -3071,6 +3091,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + vite-plugin-rewrite-all@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/vite-plugin-rewrite-all/-/vite-plugin-rewrite-all-0.1.2.tgz#312bbcd76c700ceac5153bfc5ad7e3e3e4bc9606" diff --git a/maps/tests/Metadata/customMenu.js b/maps/tests/Metadata/customMenu.js index 7cedb632..a04cb760 100644 --- a/maps/tests/Metadata/customMenu.js +++ b/maps/tests/Metadata/customMenu.js @@ -1,7 +1,7 @@ let menuIframeApi = undefined; WA.ui.registerMenuCommand('custom callback menu', () => { - WA.nav.openTab("https://workadventu.re/"); + WA.chat.sendChatMessage('Custom menu clicked', 'Mr Robot'); }) WA.ui.registerMenuCommand('custom iframe menu', {iframe: 'customIframeMenu.html'}); diff --git a/maps/tests/Metadata/setTiles.html b/maps/tests/Metadata/setTiles.php similarity index 100% rename from maps/tests/Metadata/setTiles.html rename to maps/tests/Metadata/setTiles.php diff --git a/maps/tests/Metadata/showHideLayer.html b/maps/tests/Metadata/showHideLayer.php similarity index 100% rename from maps/tests/Metadata/showHideLayer.html rename to maps/tests/Metadata/showHideLayer.php diff --git a/maps/tests/emoji.json b/maps/tests/emoji.json index 520c7b26..3fd6dc10 100644 --- a/maps/tests/emoji.json +++ b/maps/tests/emoji.json @@ -13,7 +13,7 @@ "width":10, "x":0, "y":0 - }, + }, { "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "height":10, @@ -25,7 +25,7 @@ "width":10, "x":0, "y":0 - }, + }, { "draworder":"topdown", "id":3, @@ -40,7 +40,7 @@ { "fontfamily":"Sans Serif", "pixelsize":13, - "text":"Test:\nClick on tne WOKA name\n\nResult:\nThe tab with all emoji should be displayed\n\nTest:\nChoose on emoji and click on it\n\nResult:\nEmoji will be play at the top of your WOKA", + "text":"Test:\nClick on the WOKA name\n\nResult:\nThe tab with all emojis should be displayed\n\nTest:\nChoose on emoji and click on it\n\nResult:\nEmoji will be displayed above your WOKA", "wrap":true }, "type":"", @@ -79,4 +79,4 @@ "type":"map", "version":"1.6", "width":10 -} \ No newline at end of file +} diff --git a/maps/tests/index.html b/maps/tests/index.html index 370d99e6..610337ee 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -216,7 +216,7 @@ Success Failure Pending - Test a iframe opened by a script can use Iframe API + Test a iframe opened by a script can use Iframe API (already automated in E2E tests) @@ -224,7 +224,7 @@ Success Failure Pending - Testing add a custom menu by scripting API + Testing add a custom menu by scripting API (already automated in E2E tests) diff --git a/messages/JsonMessages/AdminApiData.ts b/messages/JsonMessages/AdminApiData.ts index 5c994a11..b06d624d 100644 --- a/messages/JsonMessages/AdminApiData.ts +++ b/messages/JsonMessages/AdminApiData.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { extendApi } from "@anatine/zod-openapi"; /* * WARNING! The original file is in /messages/JsonMessages. @@ -6,10 +7,17 @@ import { z } from "zod"; */ export const isAdminApiData = z.object({ - userUuid: z.string(), - email: z.nullable(z.string()), - roomUrl: z.string(), - mapUrlStart: z.string(), + // @ts-ignore + userUuid: extendApi(z.string(), { example: "998ce839-3dea-4698-8b41-ebbdf7688ad9" }), + email: extendApi(z.nullable(z.string()), { + description: "The email of the current user.", + example: "example@workadventu.re", + }), + roomUrl: extendApi(z.string(), { example: "/@/teamSlug/worldSlug/roomSlug" }), + mapUrlStart: extendApi(z.string(), { + description: "The full URL to the JSON map file", + example: "https://myuser.github.io/myrepo/map.json", + }), messages: z.optional(z.array(z.unknown())), }); diff --git a/messages/JsonMessages/ErrorApiData.ts b/messages/JsonMessages/ErrorApiData.ts index bb029159..39334e25 100644 --- a/messages/JsonMessages/ErrorApiData.ts +++ b/messages/JsonMessages/ErrorApiData.ts @@ -1,40 +1,129 @@ import { z } from "zod"; +import { extendApi } from "@anatine/zod-openapi"; /* * WARNING! The original file is in /messages/JsonMessages. * All other files are automatically copied from this file on container startup / build */ -export const isErrorApiErrorData = z.object({ +export const isErrorApiErrorData = extendApi( // @ts-ignore - type: z.literal("error"), - code: z.string(), - title: z.string(), - subtitle: z.string(), - details: z.string(), - image: z.string(), -}); + z.object({ + type: z.literal("error"), + code: extendApi(z.string(), { + description: "The system code of an error, it must be in SCREAMING_SNAKE_CASE.", + example: "ROOM_NOT_FOUND", + }), + title: extendApi(z.string(), { description: "Big title displayed on the error screen.", example: "ERROR" }), + subtitle: extendApi(z.string(), { + description: "Subtitle displayed to let the user know what is the main subject of the error.", + example: "The room was not found.", + }), + details: extendApi(z.string(), { + description: "Some others details on what the user can do if he don't understand the error.", + example: + "If you would like more information, you can contact the administrator or us at example@workadventu.re.", + }), + image: extendApi(z.string(), { + description: "The URL of the image displayed just under the logo in the error screen.", + example: "https://example.com/error.png", + }), + }), + { + description: + 'This is an error that can be returned by the API, its type must be equal to "error".\n If such an error is caught, an error screen will be displayed.', + } +); -export const isErrorApiRetryData = z.object({ - type: z.literal("retry"), - code: z.string(), - title: z.string(), - subtitle: z.string(), - details: z.string(), - image: z.string(), - buttonTitle: z.optional(z.nullable(z.string())), - timeToRetry: z.number(), - canRetryManual: z.boolean(), -}); +export const isErrorApiRetryData = extendApi( + z.object({ + type: z.literal("retry"), + code: extendApi(z.string(), { + description: + "The system code of an error, it must be in SCREAMING_SNAKE_CASE. \n It will not be displayed to the user.", + example: "WORLD_FULL", + }), + title: extendApi(z.string(), { description: "Big title displayed on the error screen.", example: "ERROR" }), + subtitle: extendApi(z.string(), { + description: "Subtitle displayed to let the user know what is the main subject of the error.", + example: "Too successful, your WorkAdventure world is full!", + }), + details: extendApi(z.string(), { + description: "Some others details on what the user can do if he don't understand the error.", + example: "New automatic attempt in 30 seconds", + }), + image: extendApi(z.string(), { + description: "The URL of the image displayed just under the logo in the waiting screen.", + example: "https://example.com/wait.png", + }), + buttonTitle: extendApi(z.optional(z.nullable(z.string())), { + description: + "If this is not defined the button and the parameter canRetryManual is set to true, the button will be not displayed at all.", + example: "Retry", + }), + timeToRetry: extendApi(z.number(), { + description: "This is the time (in millisecond) between the next auto refresh of the page.", + example: 30_000, + }), + canRetryManual: extendApi(z.boolean(), { + description: "This boolean show or hide the button to let the user refresh manually the current page.", + example: true, + }), + }), + { + description: + 'This is an error that can be returned by the API, its type must be equal to "retry".\n' + + "If such an error is caught, a waiting screen will be displayed.", + } +); -export const isErrorApiRedirectData = z.object({ - type: z.literal("redirect"), - urlToRedirect: z.string(), -}); +export const isErrorApiRedirectData = extendApi( + z.object({ + type: z.literal("redirect"), + urlToRedirect: extendApi(z.string(), { + description: "A URL specified to redirect the user onto it directly", + example: "/contact-us", + }), + }), + { + description: + 'This is an error that can be returned by the API, its type must be equal to "redirect".\n' + + "If such an error is caught, the user will be automatically redirected to urlToRedirect.", + } +); -export const isErrorApiUnauthorizedData = z.object({ - type: z.literal("unauthorized"), -}); +export const isErrorApiUnauthorizedData = extendApi( + z.object({ + type: z.literal("unauthorized"), + code: extendApi(z.string(), { + description: "This is the system code of an error, it must be in SCREAMING_SNAKE_CASE.", + example: "USER_ACCESS_FORBIDDEN", + }), + title: extendApi(z.string(), { description: "Big title displayed on the error screen.", example: "ERROR" }), + subtitle: extendApi(z.string(), { + description: "Subtitle displayed to let the user know what is the main subject of the error.", + example: "You can't access this place.", + }), + details: extendApi(z.string(), { + description: "Some others details on what the user can do if he don't understand the error.", + example: + "If you would like more information, you can contact the administrator or us at example@workadventu.re.", + }), + image: extendApi(z.string(), { + description: "The URL of the image displayed just under the logo in the error screen.", + example: "https://example.com/error.png", + }), + buttonTitle: extendApi(z.optional(z.nullable(z.string())), { + description: "If this is not defined the button to logout will be not displayed.", + example: "Log out", + }), + }), + { + description: + 'This is an error that can be returned by the API, its type must be equal to "unauthorized".\n' + + "If such an error is caught, an error screen will be displayed with a button to let him logout and go to login page.", + } +); export const isErrorApiData = z.discriminatedUnion("type", [ isErrorApiErrorData, diff --git a/messages/JsonMessages/MapDetailsData.ts b/messages/JsonMessages/MapDetailsData.ts index 79407fdf..5df1bc7f 100644 --- a/messages/JsonMessages/MapDetailsData.ts +++ b/messages/JsonMessages/MapDetailsData.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { extendApi } from "@anatine/zod-openapi"; /* * WARNING! The original file is in /messages/JsonMessages. @@ -6,20 +7,48 @@ import { z } from "zod"; */ export const isMapDetailsData = z.object({ - mapUrl: z.string(), - authenticationMandatory: z.optional(z.nullable(z.boolean())), - group: z.nullable(z.string()), + // @ts-ignore + mapUrl: extendApi(z.string(), { + description: "The full URL to the JSON map file", + example: "https://myuser.github.io/myrepo/map.json", + }), + authenticationMandatory: extendApi(z.optional(z.nullable(z.boolean())), { + description: "Whether the authentication is mandatory or not for this map", + example: true, + }), + group: extendApi(z.nullable(z.string()), { + description: 'The group this room is part of (maps the notion of "world" in WorkAdventure SAAS)', + example: "myorg/myworld", + }), - contactPage: z.optional(z.nullable(z.string())), - iframeAuthentication: z.optional(z.nullable(z.string())), + contactPage: extendApi(z.optional(z.nullable(z.string())), { + description: "The URL to the contact page", + example: "https://mycompany.com/contact-us", + }), + iframeAuthentication: extendApi(z.optional(z.nullable(z.string())), { + description: "The URL of the authentication Iframe", + example: "https://mycompany.com/authc", + }), // The date (in ISO 8601 format) at which the room will expire - expireOn: z.optional(z.string()), + expireOn: extendApi(z.optional(z.string()), { + description: "The date (in ISO 8601 format) at which the room will expire", + example: "2022-11-05T08:15:30-05:00", + }), // Whether the "report" feature is enabled or not on this room - canReport: z.optional(z.boolean()), + canReport: extendApi(z.optional(z.boolean()), { + description: 'Whether the "report" feature is enabled or not on this room', + example: true, + }), // The URL of the logo image on the loading screen - loadingLogo: z.optional(z.nullable(z.string())), + loadingLogo: extendApi(z.optional(z.nullable(z.string())), { + description: "The URL of the image to be used on the loading page", + example: "https://example.com/logo.png", + }), // The URL of the logo image on "LoginScene" - loginSceneLogo: z.optional(z.nullable(z.string())), + loginSceneLogo: extendApi(z.optional(z.nullable(z.string())), { + description: "The URL of the image to be used on the LoginScene", + example: "https://example.com/logo_login.png", + }), }); export type MapDetailsData = z.infer; diff --git a/messages/package.json b/messages/package.json index 1dba4f5a..78f96919 100644 --- a/messages/package.json +++ b/messages/package.json @@ -18,8 +18,10 @@ "pretty-check": "yarn prettier --check 'JsonMessages/**/*.ts'" }, "dependencies": { + "@anatine/zod-openapi": "^1.3.0", "google-protobuf": "^3.13.0", "grpc": "^1.24.4", + "openapi3-ts": "^2.0.2", "ts-proto": "^1.96.0", "zod": "^3.14.3" }, diff --git a/messages/yarn.lock b/messages/yarn.lock index 131c433b..c2b538d1 100644 --- a/messages/yarn.lock +++ b/messages/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@anatine/zod-openapi@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@anatine/zod-openapi/-/zod-openapi-1.3.0.tgz#b5b38c3d821b79674226aa7b327c88c371860d0d" + integrity sha512-l54DypUdDsIq1Uwjv4ib9IBkTXMKZQLUj7qvdFL51EExC5LdSSqOlTOyaVVZZGYgWPKM7ZjGklhdoknLz4EC+w== + dependencies: + ts-deepmerge "^1.1.0" + validator "^13.7.0" + "@babel/code-frame@^7.0.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -3241,6 +3249,13 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +openapi3-ts@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-2.0.2.tgz#a200dd838bf24c9086c8eedcfeb380b7eb31e82a" + integrity sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw== + dependencies: + yaml "^1.10.2" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -4284,6 +4299,11 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" +ts-deepmerge@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-1.1.0.tgz#4236ae102199affe2e77690dcf198a420160eef2" + integrity sha512-VvwaV/6RyYMwT9d8dClmfHIsG2PCdm6WY430QKOIbPRR50Y/1Q2ilp4i2XEZeHFcNqfaYnAQzpyUC6XA0AqqBg== + ts-poet@^4.5.0: version "4.6.1" resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.6.1.tgz#015dc823d726655af9f095c900f84ed7c60e2dd3" @@ -4487,6 +4507,11 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -4593,7 +4618,7 @@ yallist@^3.0.0, yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^1.10.0: +yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== diff --git a/pusher/src/Controller/SwaggerController.ts b/pusher/src/Controller/SwaggerController.ts index 618b5266..e1e50ef3 100644 --- a/pusher/src/Controller/SwaggerController.ts +++ b/pusher/src/Controller/SwaggerController.ts @@ -1,9 +1,11 @@ import { BaseHttpController } from "./BaseHttpController"; import * as fs from "fs"; +import { ADMIN_URL } from "../Enum/EnvironmentVariable"; +import SwaggerGenerator from "../Services/SwaggerGenerator"; export class SwaggerController extends BaseHttpController { routes() { - this.app.get("/openapi", (req, res) => { + this.app.get("/openapi/pusher", (req, res) => { // Let's load the module dynamically (it may not exist in prod because part of the -dev packages) const swaggerJsdoc = require("swagger-jsdoc"); const options = { @@ -20,6 +22,43 @@ export class SwaggerController extends BaseHttpController { res.json(swaggerJsdoc(options)); }); + this.app.get("/openapi/admin", (req, res) => { + // Let's load the module dynamically (it may not exist in prod because part of the -dev packages) + const swaggerJsdoc = require("swagger-jsdoc"); + const options = { + swaggerDefinition: { + swagger: "2.0", + //openapi: "3.0.0", + info: { + title: "WorkAdventure Pusher", + version: "1.0.0", + description: + "This is a documentation about the endpoints called by the pusher. \n You can find out more about WorkAdventure on [github](https://github.com/thecodingmachine/workadventure).", + contact: { + email: "hello@workadventu.re", + }, + }, + host: "pusher." + ADMIN_URL.replace("//", ""), + tags: [ + { + name: "AdminAPI", + description: "Access to end points of the admin from the pusher", + }, + ], + securityDefinitions: { + Bearer: { + type: "apiKey", + name: "Authorization", + in: "header", + }, + }, + ...SwaggerGenerator.definitions(), + }, + apis: ["./src/Services/*.ts"], + }; + res.json(swaggerJsdoc(options)); + }); + // Create a LiveDirectory instance to virtualize directory with our assets // @ts-ignore const LiveDirectory = require("live-directory"); @@ -39,8 +78,16 @@ export class SwaggerController extends BaseHttpController { if (err) { return response.status(500).send(err.message); } - const result = data.replace(/https:\/\/petstore\.swagger\.io\/v2\/swagger.json/g, "/openapi"); + const urls = [ + { url: "/openapi/pusher", name: "Front -> Pusher" }, + { url: "/openapi/admin", name: "Pusher <- Admin" }, + ]; + + const result = data.replace( + /url: "https:\/\/petstore\.swagger\.io\/v2\/swagger.json"/g, + `urls: ${JSON.stringify(urls)}, "urls.primaryName": "Pusher <- Admin"` + ); response.send(result); return; diff --git a/pusher/src/Model/Websocket/Admin/AdminMessages.ts b/pusher/src/Model/Websocket/Admin/AdminMessages.ts index 1dc18ae5..40fc3208 100644 --- a/pusher/src/Model/Websocket/Admin/AdminMessages.ts +++ b/pusher/src/Model/Websocket/Admin/AdminMessages.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { extendApi } from "@anatine/zod-openapi"; export const isBanBannedAdminMessageInterface = z.object({ type: z.enum(["ban", "banned"]), @@ -8,7 +9,7 @@ export const isBanBannedAdminMessageInterface = z.object({ export const isUserMessageAdminMessageInterface = z.object({ event: z.enum(["user-message"]), - message: isBanBannedAdminMessageInterface, + message: extendApi(isBanBannedAdminMessageInterface, { $ref: "#/definitions/BanBannedAdminMessageInterface" }), world: z.string(), jwt: z.string(), }); diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index b8f84141..cdd2e35b 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -9,6 +9,7 @@ import qs from "qs"; import { AdminInterface } from "./AdminInterface"; import { AuthTokenData, jwtTokenManager } from "./JWTTokenManager"; import { InvalidTokenError } from "../Controller/InvalidTokenError"; +import { extendApi } from "@anatine/zod-openapi"; export interface AdminBannedData { is_banned: boolean; @@ -16,15 +17,31 @@ export interface AdminBannedData { } export const isFetchMemberDataByUuidResponse = z.object({ - email: z.string(), - userUuid: z.string(), - tags: z.array(z.string()), - visitCardUrl: z.nullable(z.string()), - textures: z.array(isWokaDetail), - messages: z.array(z.unknown()), + // @ts-ignore + email: extendApi(z.string(), { + description: "The email of the fetched user, it can be an email, an uuid or undefined.", + example: "example@workadventu.re", + }), + userUuid: extendApi(z.string(), { + description: "The uuid of the fetched user, it can be an email, an uuid or undefined.", + example: "998ce839-3dea-4698-8b41-ebbdf7688ad9", + }), + tags: extendApi(z.array(z.string()), { + description: "List of tags related to the user fetched.", + example: ["editor"], + }), + visitCardUrl: extendApi(z.nullable(z.string()), { + description: "URL of the visitCard of the user fetched.", + example: "https://mycompany.com/contact/me", + }), + textures: extendApi(z.array(isWokaDetail), { $ref: "#/definitions/WokaDetail" }), + messages: extendApi(z.array(z.unknown()), { description: "List of user's messages." }), - anonymous: z.optional(z.boolean()), - userRoomToken: z.optional(z.string()), + anonymous: extendApi(z.optional(z.boolean()), { + description: "Whether the user if logged as anonymous or not", + example: false, + }), + userRoomToken: extendApi(z.optional(z.string()), { description: "", example: "" }), }); export type FetchMemberDataByUuidResponse = z.infer; @@ -69,6 +86,47 @@ class AdminApi implements AdminInterface { userId, }; + /** + * @openapi + * /api/map: + * get: + * tags: ["AdminAPI"] + * description: Returns a map mapping map name to file name of the map + * security: + * - Bearer: [] + * produces: + * - "application/json" + * parameters: + * - name: "playUri" + * in: "query" + * description: "The full URL of WorkAdventure" + * required: true + * type: "string" + * example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug" + * - name: "userId" + * in: "query" + * description: "The identifier of the current user \n It can be undefined or an uuid or an email" + * type: "string" + * example: "998ce839-3dea-4698-8b41-ebbdf7688ad9" + * responses: + * 200: + * description: The details of the member + * schema: + * $ref: "#/definitions/MapDetailsData" + * 401: + * description: Error while retrieving the data because you are not authorized + * schema: + * $ref: '#/definitions/ErrorApiRedirectData' + * 403: + * description: Error while retrieving the data because you are not authorized + * schema: + * $ref: '#/definitions/ErrorApiUnauthorizedData' + * 404: + * description: Error while retrieving the data + * schema: + * $ref: '#/definitions/ErrorApiErrorData' + * + */ const res = await Axios.get>(ADMIN_API_URL + "/api/map", { headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" }, params, @@ -99,6 +157,58 @@ class AdminApi implements AdminInterface { characterLayers: string[], locale?: string ): Promise { + /** + * @openapi + * /api/room/access: + * get: + * tags: ["AdminAPI"] + * description: Returns member's informations if he can access this room + * security: + * - Bearer: [] + * produces: + * - "application/json" + * parameters: + * - name: "userIdentifier" + * in: "query" + * description: "The identifier of the current user \n It can be undefined or an uuid or an email" + * type: "string" + * example: "998ce839-3dea-4698-8b41-ebbdf7688ad9" + * - name: "playUri" + * in: "query" + * description: "The full URL of WorkAdventure" + * required: true + * type: "string" + * example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug" + * - name: "ipAddress" + * in: "query" + * required: true + * type: "string" + * example: "127.0.0.1" + * - name: "characterLayers" + * in: "query" + * type: "array" + * items: + * type: string + * example: ["male1"] + * responses: + * 200: + * description: The details of the member + * schema: + * $ref: "#/definitions/FetchMemberDataByUuidResponse" + * 401: + * description: Error while retrieving the data because you are not authorized + * schema: + * $ref: '#/definitions/ErrorApiRedirectData' + * 403: + * description: Error while retrieving the data because you are not authorized + * schema: + * $ref: '#/definitions/ErrorApiUnauthorizedData' + * 404: + * description: Error while retrieving the data + * schema: + * $ref: '#/definitions/ErrorApiErrorData' + * + */ const res = await Axios.get>(ADMIN_API_URL + "/api/room/access", { params: { userIdentifier, @@ -130,6 +240,42 @@ class AdminApi implements AdminInterface { playUri: string | null, locale?: string ): Promise { + /** + * @openapi + * /api/login-url/{organizationMemberToken}: + * get: + * tags: ["AdminAPI"] + * description: Returns a member from the token + * security: + * - Bearer: [] + * produces: + * - "application/json" + * parameters: + * - name: "organizationMemberToken" + * in: "path" + * description: "The token of member in the organization" + * type: "string" + * - name: "playUri" + * in: "query" + * description: "The full URL of WorkAdventure" + * required: true + * type: "string" + * example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug" + * responses: + * 200: + * description: The details of the member + * schema: + * $ref: "#/definitions/AdminApiData" + * 401: + * description: Error while retrieving the data because you are not authorized + * schema: + * $ref: '#/definitions/ErrorApiRedirectData' + * 404: + * description: Error while retrieving the data + * schema: + * $ref: '#/definitions/ErrorApiErrorData' + * + */ //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, { params: { playUri }, @@ -154,6 +300,41 @@ class AdminApi implements AdminInterface { reportWorldSlug: string, locale?: string ) { + /** + * @openapi + * /api/report: + * post: + * tags: ["AdminAPI"] + * description: Report one user with a comment + * security: + * - Bearer: [] + * produces: + * - "application/json" + * parameters: + * - name: "reportedUserUuid" + * in: "query" + * description: "The identifier of the reported user \n It can be an uuid or an email" + * type: "string" + * example: "998ce839-3dea-4698-8b41-ebbdf7688ad9" + * - name: "reportedUserComment" + * in: "query" + * description: "The comment of the report" + * required: true + * type: "string" + * - name: "reporterUserUuid" + * in: "query" + * description: "The identifier of the reporter user \n It can be an uuid or an email" + * type: "string" + * example: "998ce839-3dea-4698-8b41-ebbdf7688ad8" + * - name: "reportWorldSlug" + * in: "query" + * description: "The slug of the world where the report is made" + * type: "string" + * example: "/@/teamSlug/worldSlug/roomSlug" + * responses: + * 200: + * description: The report has been successfully saved + */ return Axios.post( `${ADMIN_API_URL}/api/report`, { @@ -174,6 +355,53 @@ class AdminApi implements AdminInterface { roomUrl: string, locale?: string ): Promise { + /** + * @openapi + * /api/ban: + * get: + * tags: ["AdminAPI"] + * description: Check if user is banned or not + * security: + * - Bearer: [] + * produces: + * - "application/json" + * parameters: + * - name: "ipAddress" + * in: "query" + * type: "string" + * required: true + * example: "127.0.0.1" + * - name: "token" + * in: "query" + * description: "The uuid of the user \n It can be an uuid or an email" + * type: "string" + * required: true + * example: "998ce839-3dea-4698-8b41-ebbdf7688ad8" + * - name: "roomUrl" + * in: "query" + * description: "The slug of the world where to check if the user is banned" + * type: "string" + * required: true + * example: "/@/teamSlug/worldSlug/roomSlug" + * responses: + * 200: + * description: The user is banned or not + * content: + * application/json: + * schema: + * type: array + * required: + * - is_banned + * properties: + * is_banned: + * type: boolean + * description: Whether the user is banned or not + * example: true + * 404: + * description: Error while retrieving the data + * schema: + * $ref: '#/definitions/ErrorApiErrorData' + */ //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. return Axios.get( ADMIN_API_URL + @@ -191,6 +419,37 @@ class AdminApi implements AdminInterface { } async getUrlRoomsFromSameWorld(roomUrl: string, locale?: string): Promise { + /** + * @openapi + * /api/room/sameWorld: + * get: + * tags: ["AdminAPI"] + * description: Get all URLs of the rooms from the world specified + * security: + * - Bearer: [] + * produces: + * - "application/json" + * parameters: + * - name: "roomUrl" + * in: "query" + * description: "The slug of the room" + * type: "string" + * required: true + * example: "/@/teamSlug/worldSlug/roomSlug" + * responses: + * 200: + * description: The list of URL of the rooms from the same world + * schema: + * type: array + * items: + * type: string + * description: URL of a room + * example: "http://example.com/@/teamSlug/worldSlug/room2Slug" + * 404: + * description: Error while retrieving the data + * schema: + * $ref: '#/definitions/ErrorApiErrorData' + */ return Axios.get(ADMIN_API_URL + "/api/room/sameWorld" + "?roomUrl=" + encodeURIComponent(roomUrl), { headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" }, }).then((data) => { @@ -204,10 +463,6 @@ class AdminApi implements AdminInterface { } return `${OPID_PROFILE_SCREEN_PROVIDER}?accessToken=${accessToken}`; } - - async logoutOauth(token: string): Promise { - await Axios.get(ADMIN_API_URL + `/oauth/logout?token=${token}`); - } } export const adminApi = new AdminApi(); diff --git a/pusher/src/Services/AdminInterface.ts b/pusher/src/Services/AdminInterface.ts index e70a653a..afdf4a9e 100644 --- a/pusher/src/Services/AdminInterface.ts +++ b/pusher/src/Services/AdminInterface.ts @@ -74,9 +74,4 @@ export interface AdminInterface { * @return string */ getProfileUrl(accessToken: string): string; - - /** - * @param token - */ - logoutOauth(token: string): Promise; } diff --git a/pusher/src/Services/LocalAdmin.ts b/pusher/src/Services/LocalAdmin.ts index 8470efb9..469966b3 100644 --- a/pusher/src/Services/LocalAdmin.ts +++ b/pusher/src/Services/LocalAdmin.ts @@ -87,10 +87,6 @@ class LocalAdmin implements AdminInterface { new Error("No admin backoffice set!"); return ""; } - - async logoutOauth(token: string): Promise { - return Promise.reject(new Error("No admin backoffice set!")); - } } export const localAdmin = new LocalAdmin(); diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index fa11a820..cca544c9 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -658,7 +658,7 @@ export class SocketManager implements ZoneEventListener { public emitErrorScreenMessage(client: compressors.WebSocket, errorApi: ErrorApiData) { const errorMessage = new ErrorScreenMessage(); errorMessage.setType(errorApi.type); - if (errorApi.type == "retry" || errorApi.type == "error") { + if (errorApi.type == "retry" || errorApi.type == "error" || errorApi.type == "unauthorized") { errorMessage.setCode(new StringValue().setValue(errorApi.code)); errorMessage.setTitle(new StringValue().setValue(errorApi.title)); errorMessage.setSubtitle(new StringValue().setValue(errorApi.subtitle)); diff --git a/pusher/src/Services/SwaggerGenerator.ts b/pusher/src/Services/SwaggerGenerator.ts new file mode 100644 index 00000000..fcda50d0 --- /dev/null +++ b/pusher/src/Services/SwaggerGenerator.ts @@ -0,0 +1,35 @@ +import { generateSchema } from "@anatine/zod-openapi"; +import { isAdminApiData } from "../Messages/JsonMessages/AdminApiData"; +import { + isErrorApiErrorData, + isErrorApiRedirectData, + isErrorApiRetryData, + isErrorApiUnauthorizedData, +} from "../Messages/JsonMessages/ErrorApiData"; +import { isMapDetailsData } from "../Messages/JsonMessages/MapDetailsData"; +import { isFetchMemberDataByUuidResponse } from "./AdminApi"; +import { isWokaDetail } from "../Messages/JsonMessages/PlayerTextures"; + +class SwaggerGenerator { + definitions() { + return { + definitions: { + AdminApiData: generateSchema(isAdminApiData), + //BanBannedAdminMessageInterface: generateSchema(isBanBannedAdminMessageInterface), + ErrorApiErrorData: generateSchema(isErrorApiErrorData), + ErrorApiRedirectData: generateSchema(isErrorApiRedirectData), + ErrorApiRetryData: generateSchema(isErrorApiRetryData), + ErrorApiUnauthorizedData: generateSchema(isErrorApiUnauthorizedData), + FetchMemberDataByUuidResponse: generateSchema(isFetchMemberDataByUuidResponse), + //ListenRoomsMessageInterface: generateSchema(isListenRoomsMessageInterface), + MapDetailsData: generateSchema(isMapDetailsData), + //RegisterData: generateSchema(isRegisterData), + //RoomRedirect: generateSchema(isRoomRedirect), + //UserMessageAdminMessageInterface: generateSchema(isUserMessageAdminMessageInterface), + WokaDetail: generateSchema(isWokaDetail), + }, + }; + } +} + +export default new SwaggerGenerator(); diff --git a/tests/tests/iframe_script.spec.ts b/tests/tests/iframe_script.spec.ts new file mode 100644 index 00000000..5da011d2 --- /dev/null +++ b/tests/tests/iframe_script.spec.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@playwright/test'; +import { login } from './utils/roles'; + +test.describe('Iframe API', () => { + test('can be called from an iframe loading a script', async ({ + page, + }) => { + await page.goto( + 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Metadata/cowebsiteAllowApi.json' + ); + + await login(page); + + await expect(page.locator('p.other-text')).toHaveText('The iframe opened by a script works !', {useInnerText: true}); + }); + + test('can add a custom menu by scripting API', async ({ + page, + }) => { + await page.goto( + 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Metadata/customMenu.json' + ); + + await login(page); + + await page.click('.menuIcon img:first-child'); + + await page.click('button:has-text("custom iframe menu")'); + + const iframeParagraph = page + .frameLocator('.menu-submenu-container iframe') + .locator('p'); + await expect(iframeParagraph).toHaveText('This is an iframe in a custom menu.'); + + await page.click('button:has-text("custom callback menu")'); + await expect(page.locator('p.other-text')).toHaveText('Custom menu clicked', {useInnerText: true}); + }); +});