diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 707df4a6..49b08bb7 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -12,14 +12,13 @@ import { WebRtcSignalToServerMessage, PlayGlobalMessage, ReportPlayerMessage, - QueryJitsiJwtMessage, - SendJitsiJwtMessage, + QueryJitsiJwtMessage } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; import {TemplatedApp} from "uWebSockets.js" import {parse} from "query-string"; import {jwtTokenManager} from "../Services/JWTTokenManager"; -import {adminApi} from "../Services/AdminApi"; +import {adminApi, fetchMemberDataByUuidResponse} from "../Services/AdminApi"; import {socketManager} from "../Services/SocketManager"; import {emitInBatch, resetPing} from "../Services/IoSocketHelpers"; import Jwt from "jsonwebtoken"; @@ -72,7 +71,33 @@ export class IoSocketController { clientEventsEmitter.registerToClientLeave(ws.clientLeaveCallback); }, message: (ws, arrayBuffer, isBinary): void => { - console.log('m', ws); //todo: add admin actions such as ban here + try { + //TODO refactor message type and data + const message: {event: string, message: {type: string, message: unknown, userUuid: string}} = + JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer))); + + if(message.event === 'user-message') { + const messageToEmit = (message.message as { message: string, type: string, userUuid: string }); + switch (message.message.type) { + case 'ban': { + socketManager.emitSendUserMessage(messageToEmit); + break; + } + case 'banned': { + const socketUser = socketManager.emitSendUserMessage(messageToEmit); + setTimeout(() => { + socketUser.close(); + }, 10000); + break; + } + default: { + break; + } + } + } + }catch (err) { + console.error(err); + } }, close: (ws, code, message) => { //todo make sure this code unregister the right listeners @@ -206,6 +231,23 @@ export class IoSocketController { const client = this.initClient(ws); //todo: into the upgrade instead? socketManager.handleJoinRoom(client); resetPing(client); + + //get data information and shwo messages + adminApi.fetchMemberDataByUuid(client.userUuid).then((res: fetchMemberDataByUuidResponse) => { + if (!res.messages) { + return; + } + res.messages.forEach((c: unknown) => { + const messageToSend = c as { type: string, message: string }; + socketManager.emitSendUserMessage({ + userUuid: client.userUuid, + type: messageToSend.type, + message: messageToSend.message + }) + }); + }).catch((err) => { + console.error('fetchMemberDataByUuid => err', err); + }); }, message: (ws, arrayBuffer, isBinary): void => { const client = ws as ExSocketInterface; diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index c9b40f03..9f51fb2e 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -9,11 +9,13 @@ export interface AdminApiData { tags: string[] policy_type: number userUuid: string + messages?: unknown[] } export interface fetchMemberDataByUuidResponse { uuid: string; tags: string[]; + messages: unknown[]; } class AdminApi { @@ -32,9 +34,9 @@ class AdminApi { params.roomSlug = roomSlug; } - const res = await Axios.get(ADMIN_API_URL+'/api/map', + const res = await Axios.get(ADMIN_API_URL + '/api/map', { - headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, + headers: {"Authorization": `${ADMIN_API_TOKEN}`}, params } ) @@ -45,7 +47,7 @@ class AdminApi { if (!ADMIN_API_URL) { return Promise.reject('No admin backoffice set!'); } - const res = await Axios.get(ADMIN_API_URL+'/membership/'+uuid, + const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid, { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } ) return res.data; @@ -61,6 +63,17 @@ class AdminApi { ) return res.data; } + + async fetchCheckUserByToken(organizationMemberToken: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('No admin backoffice set!'); + } + //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/check-user/'+organizationMemberToken, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data; + } reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) { return Axios.post(`${ADMIN_API_URL}/api/report`, { diff --git a/back/src/Services/JWTTokenManager.ts b/back/src/Services/JWTTokenManager.ts index 580140b0..f82fa001 100644 --- a/back/src/Services/JWTTokenManager.ts +++ b/back/src/Services/JWTTokenManager.ts @@ -2,6 +2,7 @@ import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; import {uuid} from "uuidv4"; import Jwt from "jsonwebtoken"; import {TokenInterface} from "../Controller/AuthenticateController"; +import {adminApi, AdminApiData} from "../Services/AdminApi"; class JWTTokenManager { @@ -32,7 +33,7 @@ class JWTTokenManager { const tokenInterface = tokenDecoded as TokenInterface; if (err) { console.error('An authentication error happened, invalid JsonWebToken.', err); - reject(new Error('An authentication error happened, invalid JsonWebToken. '+err.message)); + reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message)); return; } if (tokenDecoded === undefined) { @@ -41,12 +42,23 @@ class JWTTokenManager { return; } + //verify token if (!this.isValidToken(tokenInterface)) { reject(new Error('Authentication error, invalid token structure.')); return; } - resolve(tokenInterface.userUuid); + //verify user in admin + adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => { + resolve(tokenInterface.userUuid); + }).catch((err) => { + //anonymous user + if(err.response && err.response.status && err.response.status === 404){ + resolve(tokenInterface.userUuid); + return; + } + reject(new Error('Authentication error, invalid token structure. ' + err)); + }); }); }); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 44579123..9fd343be 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -19,7 +19,11 @@ import { UserMovesMessage, ViewportMessage, WebRtcDisconnectMessage, WebRtcSignalToClientMessage, - WebRtcSignalToServerMessage, WebRtcStartMessage, QueryJitsiJwtMessage, SendJitsiJwtMessage + WebRtcSignalToServerMessage, + WebRtcStartMessage, + QueryJitsiJwtMessage, + SendJitsiJwtMessage, + SendUserMessage } from "../Messages/generated/messages_pb"; import {PointInterface} from "../Model/Websocket/PointInterface"; import {User} from "../Model/User"; @@ -668,6 +672,25 @@ class SocketManager { client.send(serverToClientMessage.serializeBinary().buffer, true); } + + public emitSendUserMessage(messageToSend: {userUuid: string, message: string, type: string}): ExSocketInterface { + const socket = this.searchClientByUuid(messageToSend.userUuid); + if(!socket){ + throw 'socket was not found'; + } + + const sendUserMessage = new SendUserMessage(); + sendUserMessage.setMessage(messageToSend.message); + sendUserMessage.setType(messageToSend.type); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setSendusermessage(sendUserMessage); + + if (!socket.disconnecting) { + socket.send(serverToClientMessage.serializeBinary().buffer, true); + } + return socket; + } } export const socketManager = new SocketManager(); diff --git a/front/dist/index.html b/front/dist/index.html index 5984af7b..8e957965 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -125,6 +125,9 @@ + diff --git a/front/dist/resources/objects/report-message.mp3 b/front/dist/resources/objects/report-message.mp3 new file mode 100644 index 00000000..0135bfaf Binary files /dev/null and b/front/dist/resources/objects/report-message.mp3 differ diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index fa91d1e7..23a4bba6 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -654,4 +654,6 @@ div.modal-report-user{ width: 100%; text-align: left; padding: 30px; + max-width: calc(800px - 60px); /* size of modal - padding*/ } + diff --git a/front/src/Administration/TypeMessage.ts b/front/src/Administration/TypeMessage.ts new file mode 100644 index 00000000..7bd9b484 --- /dev/null +++ b/front/src/Administration/TypeMessage.ts @@ -0,0 +1,67 @@ +import {TypeMessageInterface} from "./UserMessageManager"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; + +export class TypeMessageExt implements TypeMessageInterface{ + private nbSecond = 0; + private maxNbSecond = 10; + private titleMessage = 'IMPORTANT !'; + + showMessage(message: string): void { + const div : HTMLDivElement = document.createElement('div'); + div.classList.add('modal-report-user'); + div.id = 'report-message-user'; + div.style.backgroundColor = '#000000e0'; + + const img : HTMLImageElement = document.createElement('img'); + img.src = 'resources/logos/report.svg'; + div.appendChild(img); + + const title : HTMLParagraphElement = document.createElement('p'); + title.id = 'title-report-user'; + title.innerText = `${this.titleMessage} (${this.maxNbSecond})`; + div.appendChild(title); + + const p : HTMLParagraphElement = document.createElement('p'); + p.id = 'body-report-user' + p.innerText = message; + div.appendChild(p); + + const mainSectionDiv = HtmlUtils.getElementByIdOrFail('main-container'); + mainSectionDiv.appendChild(div); + + const reportMessageAudio = HtmlUtils.getElementByIdOrFail('report-message'); + reportMessageAudio.play(); + + this.nbSecond = this.maxNbSecond; + setTimeout((c) => { + this.forMessage(title); + }, 1000); + } + + forMessage(title: HTMLParagraphElement){ + this.nbSecond -= 1; + title.innerText = `${this.titleMessage} (${this.nbSecond})`; + if(this.nbSecond > 0){ + setTimeout(() => { + this.forMessage(title); + }, 1000); + }else{ + title.innerText = this.titleMessage; + + const imgCancel : HTMLImageElement = document.createElement('img'); + imgCancel.id = 'cancel-report-user'; + imgCancel.src = 'resources/logos/close.svg'; + + const div = HtmlUtils.getElementByIdOrFail('report-message-user'); + div.appendChild(imgCancel); + imgCancel.addEventListener('click', () => { + div.remove(); + }); + } + } +} +export class Ban extends TypeMessageExt { +} + +export class Banned extends TypeMessageExt { +} \ No newline at end of file diff --git a/front/src/Administration/UserMessageManager.ts b/front/src/Administration/UserMessageManager.ts new file mode 100644 index 00000000..12022b03 --- /dev/null +++ b/front/src/Administration/UserMessageManager.ts @@ -0,0 +1,36 @@ +import {RoomConnection} from "../Connexion/RoomConnection"; +import * as TypeMessages from "./TypeMessage"; + +export interface TypeMessageInterface { + showMessage(message: string): void; +} + +export class UserMessageManager { + + typeMessages: Map = new Map(); + + constructor(private Connection: RoomConnection) { + const valueTypeMessageTab = Object.values(TypeMessages); + Object.keys(TypeMessages).forEach((value: string, index: number) => { + const typeMessageInstance: TypeMessageInterface = (new valueTypeMessageTab[index]() as TypeMessageInterface); + this.typeMessages.set(value.toLowerCase(), typeMessageInstance); + }); + this.initialise(); + } + + initialise() { + //receive signal to show message + this.Connection.receiveUserMessage((type: string, message: string) => { + this.showMessage(type, message); + }); + } + + showMessage(type: string, message: string) { + const classTypeMessage = this.typeMessages.get(type.toLowerCase()); + if (!classTypeMessage) { + console.error('Message unknown'); + return; + } + classTypeMessage.showMessage(message); + } +} \ No newline at end of file diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 375e1ded..fd2149c5 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -27,6 +27,7 @@ export enum EventMessage{ STOP_GLOBAL_MESSAGE = "stop-global-message", TELEPORT = "teleport", + USER_MESSAGE = "user-message", START_JITSI_ROOM = "start-jitsi-room", } diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 2d2d2cf8..9d04cedd 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -22,7 +22,7 @@ import { WebRtcSignalToServerMessage, WebRtcStartMessage, ReportPlayerMessage, - TeleportMessageMessage, QueryJitsiJwtMessage, SendJitsiJwtMessage + TeleportMessageMessage, QueryJitsiJwtMessage, SendJitsiJwtMessage, SendUserMessage } from "../Messages/generated/messages_pb" import {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; @@ -35,8 +35,6 @@ import { RoomJoinedMessageInterface, ViewportInterface, WebRtcDisconnectMessageInterface, WebRtcSignalReceivedMessageInterface, - WebRtcSignalSentMessageInterface, - WebRtcStartMessageInterface } from "./ConnexionModels"; export class RoomConnection implements RoomConnection { @@ -152,6 +150,8 @@ export class RoomConnection implements RoomConnection { this.dispatch(EventMessage.TELEPORT, message.getTeleportmessagemessage()); } else if (message.hasSendjitsijwtmessage()) { this.dispatch(EventMessage.START_JITSI_ROOM, message.getSendjitsijwtmessage()); + } else if (message.hasSendusermessage()) { + this.dispatch(EventMessage.USER_MESSAGE, message.getSendusermessage()); } else { throw new Error('Unknown message received'); } @@ -479,8 +479,13 @@ export class RoomConnection implements RoomConnection { }); } + public receiveUserMessage(callback: (type: string, message: string) => void) { + return this.onMessage(EventMessage.USER_MESSAGE, (message: SendUserMessage) => { + callback(message.getType(), message.getMessage()); + }); + } + public emitGlobalMessage(message: PlayGlobalMessageInterface){ - console.log('emitGlobalMessage', message); const playGlobalMessage = new PlayGlobalMessage(); playGlobalMessage.setId(message.id); playGlobalMessage.setType(message.type); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 2eb34ca0..f82a6ce2 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -51,6 +51,7 @@ import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {RoomConnection} from "../../Connexion/RoomConnection"; import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; +import {UserMessageManager} from "../../Administration/UserMessageManager"; import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; import {ResizableScene} from "../Login/ResizableScene"; import {Room} from "../../Connexion/Room"; @@ -114,6 +115,7 @@ export class GameScene extends ResizableScene implements CenterListener { private connection!: RoomConnection; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; + private UserMessageManager!: UserMessageManager; private ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; private connectionAnswerPromise: Promise; private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike) => void; @@ -600,6 +602,7 @@ export class GameScene extends ResizableScene implements CenterListener { // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic); this.GlobalMessageManager = new GlobalMessageManager(this.connection); + this.UserMessageManager = new UserMessageManager(this.connection); const self = this; this.simplePeer.registerPeerConnectionListener({ diff --git a/messages/messages.proto b/messages/messages.proto index 450def24..45872f22 100644 --- a/messages/messages.proto +++ b/messages/messages.proto @@ -178,6 +178,11 @@ message SendJitsiJwtMessage { string jwt = 2; } +message SendUserMessage{ + string type = 1; + string message = 2; +} + message ServerToClientMessage { oneof message { BatchMessage batchMessage = 1; @@ -191,5 +196,6 @@ message ServerToClientMessage { StopGlobalMessage stopGlobalMessage = 9; TeleportMessageMessage teleportMessageMessage = 10; SendJitsiJwtMessage sendJitsiJwtMessage = 11; + SendUserMessage sendUserMessage = 12; } }