Add endpoints on pusher to resolve wokas
This commit is contained in:
parent
f993aa4f5a
commit
2161a40e05
@ -13,4 +13,6 @@ Check out the [contributing guide](../../CONTRIBUTING.md)
|
||||
|
||||
## Front documentation
|
||||
|
||||
- [How to add translations](how-to-translate.md)
|
||||
- [How to add new functions in the scripting API](contributing-to-scripting-api.md)
|
||||
- [About Wokas](wokas.md)
|
||||
|
30
docs/dev/wokas.md
Normal file
30
docs/dev/wokas.md
Normal file
@ -0,0 +1,30 @@
|
||||
# About Wokas
|
||||
|
||||
Wokas are made of a set of layers (for custom wokas), or of only 1 layers (if selected from the first screen)
|
||||
|
||||
Internally, each layer has:
|
||||
|
||||
- a name
|
||||
- a URL
|
||||
|
||||
## Connection to a map
|
||||
|
||||
When a user connects to a map, it sends, as a web-socket parameter, the list of layer **names**.
|
||||
|
||||
The pusher is in charge of converting those layer names into the URLs. This way, a client cannot send any random
|
||||
URL to the pusher.
|
||||
|
||||
When the pusher receives the layer names, it validates these names and sends back the URLs + sends the names+urls to the back.
|
||||
If the layers cannot be validated, the websocket connections sends an error message and closes. The user is sent back to the "choose your Woka" screen.
|
||||
|
||||
## Getting the list of available Wokas
|
||||
|
||||
The pusher can send the list of available Wokas to the user.
|
||||
It can actually query the admin for this list, if needed (= if an admin is configured)
|
||||
|
||||
## In the pusher
|
||||
|
||||
The pusher contains a classes in charge of managing the Wokas:
|
||||
|
||||
- `LocalWokaService`: used when no admin is connected. Returns a hard-coded list of Wokas (stored in `pusher/data/woka.json`).
|
||||
- `AdminWokaService`: used to delegate the list of Wokas to the admin.
|
@ -220,6 +220,16 @@ class ConnectionManager {
|
||||
if (this.localUser.textures.length === 0) {
|
||||
this.localUser.textures = this._currentRoom.textures;
|
||||
} else {
|
||||
// TODO: the local store should NOT be used as a buffer for all the texture we were authorized to have. Bad idea.
|
||||
// Instead, it is the responsibility of the ADMIN to return the EXACT list of textures we can have in a given context
|
||||
// + this list can change over time or over rooms.
|
||||
|
||||
// 1- a room could forbid a particular dress code. In this case, the user MUST change its skin.
|
||||
// 2- a room can allow "external skins from other maps" => important: think about fediverse! => switch to URLs? (with a whitelist mechanism?) => but what about NFTs?
|
||||
|
||||
// Note: stocker des URL dans le localstorage pour les utilisateurs actuels: mauvaise idée (empêche de mettre l'URL à jour dans le futur) => en même temps, problème avec le portage de user d'un serveur à l'autre
|
||||
// Réfléchir à une notion de "character server" ??
|
||||
|
||||
this._currentRoom.textures.forEach((newTexture) => {
|
||||
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
|
||||
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
|
||||
|
1555
pusher/data/woka.json
Normal file
1555
pusher/data/woka.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@ import { PrometheusController } from "./Controller/PrometheusController";
|
||||
import { DebugController } from "./Controller/DebugController";
|
||||
import { AdminController } from "./Controller/AdminController";
|
||||
import { OpenIdProfileController } from "./Controller/OpenIdProfileController";
|
||||
import { WokaListController } from "./Controller/WokaListController";
|
||||
import HyperExpress from "hyper-express";
|
||||
import { cors } from "./Middleware/Cors";
|
||||
|
||||
@ -29,6 +30,7 @@ class App {
|
||||
new DebugController(webserver);
|
||||
new AdminController(webserver);
|
||||
new OpenIdProfileController(webserver);
|
||||
new WokaListController(webserver);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ export class AuthenticateController extends BaseHttpController {
|
||||
//if not nonce and code, user connected in anonymous
|
||||
//get data with identifier and return token
|
||||
if (!code && !nonce) {
|
||||
return res.json(JSON.stringify({ ...resUserData, authToken: token }));
|
||||
return res.json({ ...resUserData, authToken: token });
|
||||
}
|
||||
console.error("Token cannot to be check on OpenId provider");
|
||||
res.status(500);
|
||||
|
47
pusher/src/Controller/WokaListController.ts
Normal file
47
pusher/src/Controller/WokaListController.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { hasToken } from "../Middleware/HasToken";
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import { adminWokaService } from "..//Services/AdminWokaService";
|
||||
import { localWokaService } from "..//Services/LocalWokaService";
|
||||
import { WokaServiceInterface } from "src/Services/WokaServiceInterface";
|
||||
import { Server } from "hyper-express";
|
||||
|
||||
export class WokaListController extends BaseHttpController {
|
||||
private wokaService: WokaServiceInterface;
|
||||
|
||||
constructor(app: Server) {
|
||||
super(app);
|
||||
this.wokaService = ADMIN_API_URL ? adminWokaService : localWokaService;
|
||||
}
|
||||
|
||||
routes() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.app.get("/woka-list", { middlewares: [hasToken] }, async (req, res) => {
|
||||
const token = req.header("Authorization");
|
||||
const wokaList = await this.wokaService.getWokaList(token);
|
||||
|
||||
if (!wokaList) {
|
||||
return res.status(500).send("Error on getting woka list");
|
||||
}
|
||||
|
||||
return res.status(200).json(wokaList);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.app.post("/woka-details", async (req, res) => {
|
||||
const body = await req.json();
|
||||
if (!body || !body.textureIds) {
|
||||
return res.status(400);
|
||||
}
|
||||
|
||||
const textureIds = body.textureIds;
|
||||
const wokaDetails = await this.wokaService.fetchWokaDetails(textureIds);
|
||||
|
||||
if (!wokaDetails) {
|
||||
return res.json({ details: [] });
|
||||
}
|
||||
|
||||
return res.json(wokaDetails);
|
||||
});
|
||||
}
|
||||
}
|
64
pusher/src/Enum/PlayerTextures.ts
Normal file
64
pusher/src/Enum/PlayerTextures.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
//The list of all the player textures, both the default models and the partial textures used for customization
|
||||
|
||||
export const isWokaTexture = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isString,
|
||||
name: tg.isString,
|
||||
url: tg.isString,
|
||||
position: tg.isNumber,
|
||||
})
|
||||
.withOptionalProperties({
|
||||
tags: tg.isArray(tg.isString),
|
||||
tintable: tg.isBoolean,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type WokaTexture = tg.GuardedType<typeof isWokaTexture>;
|
||||
|
||||
export const isWokaTextureCollection = new tg.IsInterface()
|
||||
.withProperties({
|
||||
name: tg.isString,
|
||||
position: tg.isNumber,
|
||||
textures: tg.isArray(isWokaTexture),
|
||||
})
|
||||
.get();
|
||||
|
||||
export type WokaTextureCollection = tg.GuardedType<typeof isWokaTextureCollection>;
|
||||
|
||||
export const isWokaPartType = new tg.IsInterface()
|
||||
.withProperties({
|
||||
collections: tg.isArray(isWokaTextureCollection),
|
||||
})
|
||||
.withOptionalProperties({
|
||||
required: tg.isBoolean,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type WokaPartType = tg.GuardedType<typeof isWokaPartType>;
|
||||
|
||||
export const isWokaList = new tg.IsInterface().withStringIndexSignature(isWokaPartType).get();
|
||||
|
||||
export type WokaList = tg.GuardedType<typeof isWokaList>;
|
||||
|
||||
export const wokaPartNames = ["woka", "body", "eyes", "hair", "clothes", "hat", "accessory"];
|
||||
|
||||
export const isWokaDetail = new tg.IsInterface()
|
||||
.withProperties({
|
||||
id: tg.isString,
|
||||
})
|
||||
.withOptionalProperties({
|
||||
texture: tg.isString,
|
||||
})
|
||||
.get();
|
||||
|
||||
export type WokaDetail = tg.GuardedType<typeof isWokaDetail>;
|
||||
|
||||
export const isWokaDetailsResult = new tg.IsInterface()
|
||||
.withProperties({
|
||||
details: tg.isArray(isWokaDetail),
|
||||
})
|
||||
.get();
|
||||
|
||||
export type WokaDetailsResult = tg.GuardedType<typeof isWokaDetailsResult>;
|
16
pusher/src/Middleware/HasToken.ts
Normal file
16
pusher/src/Middleware/HasToken.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import Request from "hyper-express/types/components/http/Request";
|
||||
import Response from "hyper-express/types/components/http/Response";
|
||||
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
|
||||
|
||||
export function hasToken(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
|
||||
const authorizationHeader = req.header("Authorization");
|
||||
|
||||
if (!authorizationHeader) {
|
||||
res.status(401).send("Undefined authorization header");
|
||||
return;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
next();
|
||||
}
|
||||
}
|
71
pusher/src/Services/AdminWokaService.ts
Normal file
71
pusher/src/Services/AdminWokaService.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import axios from "axios";
|
||||
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||
import { isWokaDetailsResult, isWokaList, WokaDetailsResult, WokaList } from "../Enum/PlayerTextures";
|
||||
import { WokaServiceInterface } from "./WokaServiceInterface";
|
||||
|
||||
class AdminWokaService implements WokaServiceInterface {
|
||||
/**
|
||||
* Returns the list of all available Wokas for the current user.
|
||||
*/
|
||||
getWokaList(token: string): Promise<WokaList | undefined> {
|
||||
return axios
|
||||
.get(`${ADMIN_API_URL}/api/woka-list/${token}`, {
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||
})
|
||||
.then((res) => {
|
||||
if (isWokaList(res.data)) {
|
||||
throw new Error("Bad response format provided by woka list endpoint");
|
||||
}
|
||||
return res.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Cannot get woka list from admin API with token: ${token}`, err);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of all the images for the given texture ids.
|
||||
*
|
||||
* Key: texture id
|
||||
* Value: URL
|
||||
*
|
||||
* If one of the textures cannot be found, undefined is returned
|
||||
*/
|
||||
fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined> {
|
||||
return axios
|
||||
.post(
|
||||
`${ADMIN_API_URL}/api/woka-details`,
|
||||
{
|
||||
textureIds,
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (isWokaDetailsResult(res.data)) {
|
||||
throw new Error("Bad response format provided by woka detail endpoint");
|
||||
}
|
||||
|
||||
const result: WokaDetailsResult = res.data;
|
||||
if (result.details.length !== textureIds.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const detail of result.details) {
|
||||
if (!detail.texture) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return res.data;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Cannot get woka details from admin API with ids: ${textureIds}`, err);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const adminWokaService = new AdminWokaService();
|
64
pusher/src/Services/LocalWokaService.ts
Normal file
64
pusher/src/Services/LocalWokaService.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { WokaDetail, WokaDetailsResult, WokaList, wokaPartNames } from "../Enum/PlayerTextures";
|
||||
import { WokaServiceInterface } from "./WokaServiceInterface";
|
||||
|
||||
class LocalWokaService implements WokaServiceInterface {
|
||||
/**
|
||||
* Returns the list of all available Wokas & Woka Parts for the current user.
|
||||
*/
|
||||
async getWokaList(token: string): Promise<WokaList | undefined> {
|
||||
const wokaData: WokaList = await require("../../data/woka.json");
|
||||
if (!wokaData) {
|
||||
return undefined;
|
||||
}
|
||||
return wokaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of all the images for the given texture ids.
|
||||
*
|
||||
* Key: texture id
|
||||
* Value: URL
|
||||
*
|
||||
* If one of the textures cannot be found, undefined is returned (and the user should be redirected to Woka choice page!)
|
||||
*/
|
||||
async fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined> {
|
||||
const wokaData: WokaList = await require("../../data/woka.json");
|
||||
const textures = new Map<string, string>();
|
||||
const searchIds = new Set(textureIds);
|
||||
|
||||
for (const part of wokaPartNames) {
|
||||
const wokaPartType = wokaData[part];
|
||||
if (!wokaPartType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const collection of wokaPartType.collections) {
|
||||
for (const id of searchIds) {
|
||||
const texture = collection.textures.find((texture) => texture.id === id);
|
||||
|
||||
if (texture) {
|
||||
textures.set(id, texture.url);
|
||||
searchIds.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (textureIds.length !== textures.size) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const details: WokaDetail[] = [];
|
||||
|
||||
textures.forEach((value, key) => {
|
||||
details.push({
|
||||
id: key,
|
||||
texture: value,
|
||||
});
|
||||
});
|
||||
|
||||
return { details };
|
||||
}
|
||||
}
|
||||
|
||||
export const localWokaService = new LocalWokaService();
|
18
pusher/src/Services/WokaServiceInterface.ts
Normal file
18
pusher/src/Services/WokaServiceInterface.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { WokaDetailsResult, WokaList } from "../Enum/PlayerTextures";
|
||||
|
||||
export interface WokaServiceInterface {
|
||||
/**
|
||||
* Returns the list of all available Wokas for the current user.
|
||||
*/
|
||||
getWokaList(token: string): Promise<WokaList | undefined>;
|
||||
|
||||
/**
|
||||
* Returns the URL of all the images for the given texture ids.
|
||||
*
|
||||
* Key: texture id
|
||||
* Value: URL
|
||||
*
|
||||
* If one of the textures cannot be found, undefined is returned (and the user should be redirected to Woka choice page!)
|
||||
*/
|
||||
fetchWokaDetails(textureIds: string[]): Promise<WokaDetailsResult | undefined>;
|
||||
}
|
Loading…
Reference in New Issue
Block a user