Add endpoints on pusher to resolve wokas

This commit is contained in:
Alexis Faizeau 2022-02-17 15:02:11 +01:00 committed by David Négrier
parent f993aa4f5a
commit 2161a40e05
12 changed files with 1880 additions and 1 deletions

View File

@ -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
View 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.

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}

View File

@ -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);

View 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);
});
}
}

View 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>;

View 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();
}
}

View 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();

View 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();

View 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>;
}