From 7c20066b63762dcb6f78b30f9a39ce8c9c22ca56 Mon Sep 17 00:00:00 2001 From: NigeParis Date: Sat, 13 Dec 2025 07:15:09 +0100 Subject: [PATCH] Started Pong simple copy Chat - WIP simplification first job --- docker-compose.yml | 26 + frontend/src/pages/index.ts | 1 + .../src/pages/pong/actionBtnPopUpBlock.ts | 13 + .../src/pages/pong/actionBtnPopUpClear.ts | 13 + frontend/src/pages/pong/addMessage.ts | 18 + frontend/src/pages/pong/blockUser.ts | 11 + frontend/src/pages/pong/broadcastMsg.ts | 28 + frontend/src/pages/pong/clearChatWindow.ts | 13 + frontend/src/pages/pong/getProfil.ts | 24 + frontend/src/pages/pong/isLoggedIn.ts | 9 + frontend/src/pages/pong/listBuddies.ts | 45 ++ frontend/src/pages/pong/openProfilePopup.ts | 24 + frontend/src/pages/pong/pong.html | 54 ++ frontend/src/pages/pong/pong.ts | 615 ++++++++++++++++++ frontend/src/pages/pong/types_front.ts | 23 + frontend/src/pages/pong/windowStateHidden.ts | 18 + nginx/conf/locations/pong.conf | 14 + src/pong/.dockerignore | 2 + src/pong/README.md | 8 + src/pong/entrypoint.sh | 8 + src/pong/extra/.gitkeep | 0 src/pong/openapi.json | 21 + src/pong/package.json | 38 ++ src/pong/src/app.ts | 441 +++++++++++++ src/pong/src/broadcast.ts | 22 + src/pong/src/broadcastNextGame.ts | 25 + src/pong/src/chat_types.ts | 23 + src/pong/src/connectedUser.ts | 48 ++ src/pong/src/createNextGame.ts | 6 + src/pong/src/getUserByName.ts | 12 + src/pong/src/isBlocked.ts | 17 + src/pong/src/list_SocketListener.ts | 29 + src/pong/src/makeProfil.ts | 41 ++ src/pong/src/nextGame_SocketListener.ts | 20 + src/pong/src/openapi.ts | 21 + src/pong/src/plugins/README.md | 16 + src/pong/src/plugins/sensible.ts | 10 + src/pong/src/plugins/socket.ts | 31 + src/pong/src/routes/broadcast.ts | 57 ++ src/pong/src/run.ts | 36 + src/pong/src/sendBlocked.ts | 29 + src/pong/src/sendGameLinkToChatService.ts | 7 + src/pong/src/sendInvite.ts | 30 + src/pong/src/sendPrivMessage.ts | 39 ++ src/pong/src/sendProfil.ts | 19 + src/pong/src/setGameLink.ts | 7 + src/pong/tsconfig.json | 15 + src/pong/vite.config.js | 53 ++ 48 files changed, 2080 insertions(+) create mode 100644 frontend/src/pages/pong/actionBtnPopUpBlock.ts create mode 100644 frontend/src/pages/pong/actionBtnPopUpClear.ts create mode 100644 frontend/src/pages/pong/addMessage.ts create mode 100644 frontend/src/pages/pong/blockUser.ts create mode 100644 frontend/src/pages/pong/broadcastMsg.ts create mode 100644 frontend/src/pages/pong/clearChatWindow.ts create mode 100644 frontend/src/pages/pong/getProfil.ts create mode 100644 frontend/src/pages/pong/isLoggedIn.ts create mode 100644 frontend/src/pages/pong/listBuddies.ts create mode 100644 frontend/src/pages/pong/openProfilePopup.ts create mode 100644 frontend/src/pages/pong/pong.html create mode 100644 frontend/src/pages/pong/pong.ts create mode 100644 frontend/src/pages/pong/types_front.ts create mode 100644 frontend/src/pages/pong/windowStateHidden.ts create mode 100644 nginx/conf/locations/pong.conf create mode 100644 src/pong/.dockerignore create mode 100644 src/pong/README.md create mode 100644 src/pong/entrypoint.sh create mode 100644 src/pong/extra/.gitkeep create mode 100644 src/pong/openapi.json create mode 100644 src/pong/package.json create mode 100644 src/pong/src/app.ts create mode 100644 src/pong/src/broadcast.ts create mode 100644 src/pong/src/broadcastNextGame.ts create mode 100644 src/pong/src/chat_types.ts create mode 100644 src/pong/src/connectedUser.ts create mode 100644 src/pong/src/createNextGame.ts create mode 100644 src/pong/src/getUserByName.ts create mode 100644 src/pong/src/isBlocked.ts create mode 100644 src/pong/src/list_SocketListener.ts create mode 100644 src/pong/src/makeProfil.ts create mode 100644 src/pong/src/nextGame_SocketListener.ts create mode 100644 src/pong/src/openapi.ts create mode 100644 src/pong/src/plugins/README.md create mode 100644 src/pong/src/plugins/sensible.ts create mode 100644 src/pong/src/plugins/socket.ts create mode 100644 src/pong/src/routes/broadcast.ts create mode 100644 src/pong/src/run.ts create mode 100644 src/pong/src/sendBlocked.ts create mode 100644 src/pong/src/sendGameLinkToChatService.ts create mode 100644 src/pong/src/sendInvite.ts create mode 100644 src/pong/src/sendPrivMessage.ts create mode 100644 src/pong/src/sendProfil.ts create mode 100644 src/pong/src/setGameLink.ts create mode 100644 src/pong/tsconfig.json create mode 100644 src/pong/vite.config.js diff --git a/docker-compose.yml b/docker-compose.yml index febf81b..3b1a4d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -138,6 +138,32 @@ services: gelf-address: "udp://127.0.0.1:12201" tag: "{{.Name}}" + ############### + # PONG # + ############### + pong: + build: + context: ./src/ + args: + - SERVICE=pong + - EXTRA_FILES=pong/extra + container_name: app-pong + restart: always + networks: + - transcendance-network + volumes: + - sqlite-volume:/volumes/database + - static-volume:/volumes/static + environment: + - JWT_SECRET=KRUGKIDROVUWG2ZAMJZG653OEBTG66BANJ2W24DTEBXXMZLSEB2GQZJANRQXU6JA + - DATABASE_DIR=/volumes/database + - PROVIDER_FILE=/extra/providers.toml + - SESSION_MANAGER=${SESSION_MANAGER} + + + ############### + # USER # + ############### user: build: context: ./src/ diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 92cebd5..f0b1f2b 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -1,6 +1,7 @@ import { setTitle, handleRoute } from '@app/routing'; import './root/root.ts' import './chat/chat.ts' +import './pong/pong.ts' import './login/login.ts' import './signin/signin.ts' import './ttt/ttt.ts' diff --git a/frontend/src/pages/pong/actionBtnPopUpBlock.ts b/frontend/src/pages/pong/actionBtnPopUpBlock.ts new file mode 100644 index 0000000..0e65bcb --- /dev/null +++ b/frontend/src/pages/pong/actionBtnPopUpBlock.ts @@ -0,0 +1,13 @@ +import { Socket } from 'socket.io-client'; +import type { ClientProfil } from './types_front'; +import { blockUser } from './blockUser'; + +export function actionBtnPopUpBlock(block: ClientProfil, senderSocket: Socket) { + setTimeout(() => { + const blockUserBtn = document.querySelector("#popup-b-block"); + blockUserBtn?.addEventListener("click", () => { + block.text = ''; + blockUser(block, senderSocket); + }); + }, 0) +}; \ No newline at end of file diff --git a/frontend/src/pages/pong/actionBtnPopUpClear.ts b/frontend/src/pages/pong/actionBtnPopUpClear.ts new file mode 100644 index 0000000..a88cda8 --- /dev/null +++ b/frontend/src/pages/pong/actionBtnPopUpClear.ts @@ -0,0 +1,13 @@ +import { clearChatWindow } from './clearChatWindow'; +import { Socket } from 'socket.io-client'; +import type { ClientProfil } from './types_front'; + + +export function actionBtnPopUpClear(profil: ClientProfil, senderSocket: Socket) { + setTimeout(() => { + const clearTextBtn = document.querySelector("#popup-b-clear"); + clearTextBtn?.addEventListener("click", () => { + clearChatWindow(senderSocket); + }); + }, 0) +}; \ No newline at end of file diff --git a/frontend/src/pages/pong/addMessage.ts b/frontend/src/pages/pong/addMessage.ts new file mode 100644 index 0000000..16165db --- /dev/null +++ b/frontend/src/pages/pong/addMessage.ts @@ -0,0 +1,18 @@ +import { color } from './pong'; + +/** + * function adds a message to the frontend chatWindow + * @param text + * @returns + */ + +export function addMessage(text: string) { + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + if (!chatWindow) return; + const messageElement = document.createElement("div-test"); + messageElement.textContent = text; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + console.log(`%c DEBUG LOG: Added new message:%c ${text}`, color.red, color.reset); + return ; +}; \ No newline at end of file diff --git a/frontend/src/pages/pong/blockUser.ts b/frontend/src/pages/pong/blockUser.ts new file mode 100644 index 0000000..5f5b966 --- /dev/null +++ b/frontend/src/pages/pong/blockUser.ts @@ -0,0 +1,11 @@ +import { Socket } from 'socket.io-client'; +import type { ClientProfil } from './types_front'; +import { getUser } from "@app/auth"; + + +export function blockUser(profil: ClientProfil, senderSocket: Socket) { + profil.SenderName = getUser()?.name ?? ''; + if (profil.SenderName === profil.user) return; + // addMessage(`${profil.Sendertext}: ${profil.user}⛔`) + senderSocket.emit('blockUser', JSON.stringify(profil)); +}; \ No newline at end of file diff --git a/frontend/src/pages/pong/broadcastMsg.ts b/frontend/src/pages/pong/broadcastMsg.ts new file mode 100644 index 0000000..51a1130 --- /dev/null +++ b/frontend/src/pages/pong/broadcastMsg.ts @@ -0,0 +1,28 @@ +import { addMessage } from "./addMessage"; +import { Socket } from 'socket.io-client'; +import { getUser } from "@app/auth"; + +/** + * function sends socket.emit to the backend to active and broadcast a message to all sockets + * echos the message with addMessage to the sender + * @param socket + * @param msgCommand + */ +export function broadcastMsg (socket: Socket, msgCommand: string[]): void { + let msgText = msgCommand[1] ?? ""; + addMessage(msgText); + const user = getUser(); + if (user && socket?.connected) { + const message = { + command: msgCommand, + destination: '', + type: "chat", + user: user.name, + token: document.cookie, + text: msgText, + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + socket.emit('message', JSON.stringify(message)); + } +}; diff --git a/frontend/src/pages/pong/clearChatWindow.ts b/frontend/src/pages/pong/clearChatWindow.ts new file mode 100644 index 0000000..696a007 --- /dev/null +++ b/frontend/src/pages/pong/clearChatWindow.ts @@ -0,0 +1,13 @@ +import io, { Socket } from 'socket.io-client'; + +/** + * function clears all messages in the chat window + * @param senderSocket + * @returns + */ +export function clearChatWindow(senderSocket: Socket) { + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + if (!chatWindow) return; + chatWindow.innerHTML = ""; + // senderSocket.emit('nextGame'); +} \ No newline at end of file diff --git a/frontend/src/pages/pong/getProfil.ts b/frontend/src/pages/pong/getProfil.ts new file mode 100644 index 0000000..1ad7edf --- /dev/null +++ b/frontend/src/pages/pong/getProfil.ts @@ -0,0 +1,24 @@ +import { Socket } from 'socket.io-client'; + +/** + * getProfil of a user + * @param socket + * @param user + * @returns + */ + +export function getProfil(socket: Socket, user: string) { + if (!socket.connected) return; + const profil = { + command: '@profil', + destination: 'profilMessage', + type: "chat", + user: user, + token: document.cookie ?? "", + text: user, + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + // addMessage(JSON.stringify(profil)); + socket.emit('profilMessage', JSON.stringify(profil)); +} diff --git a/frontend/src/pages/pong/isLoggedIn.ts b/frontend/src/pages/pong/isLoggedIn.ts new file mode 100644 index 0000000..2f01067 --- /dev/null +++ b/frontend/src/pages/pong/isLoggedIn.ts @@ -0,0 +1,9 @@ +import { getUser } from "@app/auth"; +import type { User } from '@app/auth' +/** + * function checks if logged in + * @returns either user | null + */ +export function isLoggedIn(): User | null { + return getUser() || null; +}; \ No newline at end of file diff --git a/frontend/src/pages/pong/listBuddies.ts b/frontend/src/pages/pong/listBuddies.ts new file mode 100644 index 0000000..6b53ea9 --- /dev/null +++ b/frontend/src/pages/pong/listBuddies.ts @@ -0,0 +1,45 @@ +import { getUser } from "@app/auth"; +import { Socket } from 'socket.io-client'; +import { getProfil } from './getProfil'; + +/** + * function adds a user to the ping Buddies window\ + * it also acts as click or double click\ + * activates two possible actions:\ + * click => private Mag\ + * dbl click => get Profil of the name\ + * collected in the clipBoard + * @param socket + * @param buddies + * @param listBuddies + * @returns + */ + +export async function listBuddies(socket: Socket, buddies: HTMLDivElement, listBuddies: string) { + + if (!buddies) return; + const sendtextbox = document.getElementById('t-chat-window') as HTMLButtonElement; + const buddiesElement = document.createElement("div-buddies-list"); + buddiesElement.textContent = listBuddies + '\n'; + const user = getUser()?.name ?? ""; + buddies.appendChild(buddiesElement); + buddies.scrollTop = buddies.scrollHeight; + console.log(`Added buddies: ${listBuddies}`); + + buddiesElement.style.cursor = "pointer"; + buddiesElement.addEventListener("click", () => { + navigator.clipboard.writeText(listBuddies); + if (listBuddies !== user && user !== "") { + sendtextbox.value = `@${listBuddies}: `; + console.log("Copied to clipboard:", listBuddies); + sendtextbox.focus(); + } + }); + + buddiesElement.addEventListener("dblclick", () => { + console.log("Open profile:", listBuddies); + getProfil(socket, listBuddies); + sendtextbox.value = ""; + }); + +} diff --git a/frontend/src/pages/pong/openProfilePopup.ts b/frontend/src/pages/pong/openProfilePopup.ts new file mode 100644 index 0000000..4ffe17d --- /dev/null +++ b/frontend/src/pages/pong/openProfilePopup.ts @@ -0,0 +1,24 @@ +import type { ClientProfil } from './types_front'; + +export async function openProfilePopup(profil: ClientProfil) { + const modalname = document.getElementById("modal-name") ?? null; + if (modalname) + modalname.innerHTML = + ` +
+ Profil of ${profil.user}
+ Login Name: '${profil.loginName ?? 'Guest'}' +
+ Login ID: '${profil.userID ?? ''}' +
+ + + +
About: '${profil.text}'
+ + `; + const profilList = document.getElementById("profile-modal") ?? null; + if (profilList) + profilList.classList.remove("hidden"); + // The popup now exists → attach the event +} \ No newline at end of file diff --git a/frontend/src/pages/pong/pong.html b/frontend/src/pages/pong/pong.html new file mode 100644 index 0000000..68410d7 --- /dev/null +++ b/frontend/src/pages/pong/pong.html @@ -0,0 +1,54 @@ +
+
+ + +

+ ChatterBox +


+ + + + +
System: connecting ...
+
+ + +
+ +
+
+
+ + +
+
+ +
+

Ping Buddies

+
+
+
+
+ + +
+
+
+
+
+ + + + diff --git a/frontend/src/pages/pong/pong.ts b/frontend/src/pages/pong/pong.ts new file mode 100644 index 0000000..654f706 --- /dev/null +++ b/frontend/src/pages/pong/pong.ts @@ -0,0 +1,615 @@ +import { addRoute, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; +import { showError } from "@app/toast"; +import authHtml from './pong.html?raw'; +import client from '@app/api' +import { getUser, updateUser } from "@app/auth"; +import io, { Socket } from 'socket.io-client'; +import { listBuddies } from './listBuddies'; +import { getProfil } from './getProfil'; +import { addMessage } from './addMessage'; +import { broadcastMsg } from './broadcastMsg'; +import { isLoggedIn } from './isLoggedIn'; +import type { ClientMessage, ClientProfil } from './types_front'; +import { openProfilePopup } from './openProfilePopup'; +import { actionBtnPopUpClear } from './actionBtnPopUpClear'; +import { actionBtnPopUpBlock } from './actionBtnPopUpBlock'; +import { windowStateHidden } from './windowStateHidden'; + +export const color = { + red: 'color: red;', + green: 'color: green;', + yellow: 'color: orange;', + blue: 'color: blue;', + reset: '', +}; + +// get the name of the machine used to connect +const machineHostName = window.location.hostname; +console.log('connect to login at %chttps://' + machineHostName + ':8888/app/login',color.yellow); + +export let __socket: Socket | undefined = undefined; +document.addEventListener('ft:pageChange', () => { + if (__socket !== undefined) + __socket.close(); + __socket = undefined; + console.log("Page changed"); +}); + +export function getSocket(): Socket { + let addressHost = `wss://${machineHostName}:8888`; + // let addressHost = `wss://localhost:8888`; + if (__socket === undefined) + + __socket = io(addressHost, { + path: "/api/pong/socket.io/", + secure: false, + transports: ["websocket"], + }); + return __socket; +}; + +function inviteToPlayPong(profil: ClientProfil, senderSocket: Socket) { + profil.SenderName = getUser()?.name ?? ''; + if (profil.SenderName === profil.user) return; + addMessage(`You invited to play: ${profil.user}🏓`) + senderSocket.emit('inviteGame', JSON.stringify(profil)); +}; + +function actionBtnPopUpInvite(invite: ClientProfil, senderSocket: Socket) { + setTimeout(() => { + const InvitePongBtn = document.querySelector("#popup-b-invite"); + InvitePongBtn?.addEventListener("click", () => { + inviteToPlayPong(invite, senderSocket); + }); + }, 0) +}; + +// async function windowStateHidden() { +// const socketId = __socket || undefined; +// // let oldName = localStorage.getItem("oldName") ?? undefined; +// let oldName: string; +// if (socketId === undefined) return; +// let userName = await updateUser(); +// oldName = userName?.name ?? ""; +// if (oldName === "") return; +// localStorage.setItem('oldName', oldName); +// socketId.emit('client_left', { +// user: userName?.name, +// why: 'tab window hidden - socket not dead', +// }); +// return; +// }; + +async function windowStateVisable() { + + const buddies = document.getElementById('div-buddies') as HTMLDivElement; + const socketId = __socket || undefined; + let oldName = localStorage.getItem("oldName") || undefined; + console.log("%c WINDOW VISIBLE - oldName :'" + oldName + "'", color.green); + + if (socketId === undefined || oldName === undefined) {console.log("%SOCKET ID", color.red); return;} + let user = await updateUser(); + if(user === null) return; + console.log("%cUserName :'" + user?.name + "'", color.green); + socketId.emit('client_entered', { + userName: oldName, + user: user?.name, + }); + buddies.innerHTML = ''; + buddies.textContent = ''; + //connected(socketId); + setTitle('Chat Page'); + return; +}; + +function parseCmdMsg(msgText: string): string[] | undefined { + + if (!msgText?.trim()) return; + msgText = msgText.trim(); + const command: string[] = ['', '']; + if (!msgText.startsWith('@')) { + command[0] = '@msg'; + command[1] = msgText; + return command; + } + const noArgCommands = ['@quit', '@who', '@cls']; + if (noArgCommands.includes(msgText)) { + command[0] = msgText; + command[1] = ''; + return command; + } + + const ArgCommands = ['@profil', '@block']; + const userName = msgText.indexOf(" "); + const cmd2 = msgText.slice(0, userName).trim() ?? ""; + const user = msgText.slice(userName + 1).trim(); + if (ArgCommands.includes(cmd2)) { + command[0] = cmd2; + command[1] = user; + return command; + } + const colonIndex = msgText.indexOf(":"); + if (colonIndex === -1) { + command[0] = msgText; + command[1] = ''; + return command; + } + const cmd = msgText.slice(0, colonIndex).trim(); + const rest = msgText.slice(colonIndex + 1).trim(); + command[0] = cmd; + command[1] = rest; + return command; +} + +// async function listBuddies(socket: Socket, buddies: HTMLDivElement, listBuddies: string) { + +// if (!buddies) return; +// const sendtextbox = document.getElementById('t-chat-window') as HTMLButtonElement; +// const buddiesElement = document.createElement("div-buddies-list"); +// buddiesElement.textContent = listBuddies + '\n'; +// const user = getUser()?.name ?? ""; +// buddies.appendChild(buddiesElement); +// buddies.scrollTop = buddies.scrollHeight; +// console.log(`Added buddies: ${listBuddies}`); + +// buddiesElement.style.cursor = "pointer"; +// buddiesElement.addEventListener("click", () => { +// navigator.clipboard.writeText(listBuddies); +// if (listBuddies !== user && user !== "") { +// sendtextbox.value = `@${listBuddies}: `; +// console.log("Copied to clipboard:", listBuddies); +// sendtextbox.focus(); +// } +// }); + +// buddiesElement.addEventListener("dblclick", () => { +// console.log("Open profile:", listBuddies); +// getProfil(socket, listBuddies); +// sendtextbox.value = ""; +// }); + +// } + + +function waitSocketConnected(socket: Socket): Promise { + return new Promise(resolve => { + if (socket.connected) return resolve(); + socket.on("connect", () => resolve()); + }); +}; + +function quitChat (socket: Socket) { + + try { + const systemWindow = document.getElementById('system-box') as HTMLDivElement; + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + if (socket) { + logout(socket); + setTitle('Pong Page'); + systemWindow.innerHTML = ""; + chatWindow.textContent = ""; + connected(socket); + } else { + getSocket(); + } + } catch (e) { + console.error("Quit Chat error:", e); + showError('Failed to Quit Chat: Unknown error'); + } + +}; + +// const bconnected = document.getElementById('b-help') as HTMLButtonElement; +// if (bconnected) { +// bconnected.click(); +// } + +function logout(socket: Socket) { + socket.emit("logout"); // notify server + socket.disconnect(); // actually close the socket + localStorage.clear(); + if (__socket !== undefined) + __socket.close(); +// window.location.href = "/login"; +}; + + +async function connected(socket: Socket): Promise { + + try { + const buddies = document.getElementById('div-buddies') as HTMLDivElement; + const loggedIn = isLoggedIn(); + if (!loggedIn) throw('Not Logged in'); + console.log('%cloggedIn:',color.blue, loggedIn?.name); + let oldUser = localStorage.getItem("oldName") ?? ""; + console.log('%coldUser:',color.yellow, oldUser); + if (loggedIn?.name === undefined) {console.log('');return ;} + setTimeout(() => { + oldUser = loggedIn.name ?? ""; + }, 0); + // const res = await client.guestLogin(); + let user = await updateUser(); + console.log('%cUser?name:',color.yellow, user?.name); + localStorage.setItem("oldName", oldUser); + buddies.textContent = ""; + socket.emit('list', { + oldUser: oldUser, + user: user?.name, + }); + } catch (e) { + console.error("Login error:", e); + showError('Failed to login: Unknown error'); + } +}; + +async function whoami(socket: Socket) { + try { + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + const loggedIn = isLoggedIn(); + + const res = await client.guestLogin(); + switch (res.kind) { + case 'success': { + let user = await updateUser(); + if (chatWindow) { + socket.emit('updateClientName', { + oldUser: '', + user: user?.name + }); + } + if (user === null) + return showError('Failed to get user: no user ?'); + setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`); + break; + } + case 'failed': { + showError(`Failed to login: ${res.msg}`); + } + } + } catch (e) { + console.error("Login error:", e); + showError('Failed to login: Unknown error'); + } +}; + +// async function openProfilePopup(profil: ClientProfil) { + + +// const modalname = document.getElementById("modal-name") ?? null; +// if (modalname) +// modalname.innerHTML = ` +//
+// Profil of ${profil.user}
+// Login Name: '${profil.loginName ?? 'Guest'}' +//
+// Login ID: '${profil.userID ?? ''}' +//
+// +// +// +//
About: '${profil.text}'
+// +// `; +// const profilList = document.getElementById("profile-modal") ?? null; +// if (profilList) +// profilList.classList.remove("hidden"); +// // The popup now exists → attach the event +// } + +let count = 0; +function incrementCounter(): number { + count += 1; + return count; +} + +async function openMessagePopup(message: string) { + + const modalmessage = document.getElementById("modal-message") ?? null; + if(!message) return + const obj:any = JSON.parse(message); + if (modalmessage) { + const messageElement = document.createElement("div"); + messageElement.innerHTML = ` +
+
Next Game Message ${incrementCounter()}: ${obj.link}
+
`; + modalmessage.appendChild(messageElement); + modalmessage.scrollTop = modalmessage.scrollHeight; + + } + const gameMessage = document.getElementById("game-modal") ?? null; + if (gameMessage) + gameMessage.classList.remove("hidden"); + // The popup now exists → attach the event +} + + + + +function handleChat(_url: string, _args: RouteHandlerParams): RouteHandlerReturn { + + + let socket = getSocket(); + + // Listen for the 'connect' event + socket.on("connect", async () => { + + const systemWindow = document.getElementById('system-box') as HTMLDivElement; + await waitSocketConnected(socket); + console.log("I AM Connected to the server:", socket.id); + const user = getUser()?.name; + // Ensure we have a user AND socket is connected + if (!user || !socket.connected) return; + const message = { + command: "", + destination: 'system-info', + type: "chat", + user, + token: document.cookie ?? "", + text: " has Just ARRIVED in the chat", + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + socket.emit('message', JSON.stringify(message)); + const messageElement = document.createElement("div"); + messageElement.textContent = `${user}: is connected au server`; + systemWindow.appendChild(messageElement); + systemWindow.scrollTop = systemWindow.scrollHeight; + }); + + // Listen for messages from the server "MsgObjectServer" + socket.on("MsgObjectServer", (data: { message: ClientMessage}) => { + // Display the message in the chat window + const systemWindow = document.getElementById('system-box') as HTMLDivElement; + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + const bconnected = document.getElementById('b-help') as HTMLButtonElement; + + if (bconnected) { + connected(socket); + } + + if (chatWindow && data.message.destination === "") { + const messageElement = document.createElement("div"); + messageElement.textContent = `${data.message.user}: ${data.message.text}`; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + } + if (chatWindow && data.message.destination === "privateMsg") { + const messageElement = document.createElement("div-private"); + messageElement.textContent = `🔒${data.message.user}: ${data.message.text}`; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + } + + + const MAX_SYSTEM_MESSAGES = 10; + + if (systemWindow && data.message.destination === "system-info") { + const messageElement = document.createElement("div"); + messageElement.textContent = `${data.message.user}: ${data.message.text}`; + systemWindow.appendChild(messageElement); + + // keep only last 10 + while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) { + systemWindow.removeChild(systemWindow.firstChild!); + } + systemWindow.scrollTop = systemWindow.scrollHeight; + } + console.log("Getuser():", getUser()); + }); + + socket.on('profilMessage', (profil: ClientProfil) => { + openProfilePopup(profil); + actionBtnPopUpClear(profil, socket); + actionBtnPopUpInvite(profil, socket); + actionBtnPopUpBlock(profil, socket); + }); + + socket.on('inviteGame', (invite: ClientProfil) => { + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + const messageElement = document.createElement("div"); + messageElement.innerHTML =`🏓${invite.SenderName}: ${invite.innerHtml}`; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + }); + + + socket.on('blockUser', (blocked: ClientProfil) => { + let icon = '⛔'; + const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; + const messageElement = document.createElement("div"); + if (`${blocked.text}` === '\'I have un-blocked you\'' ) { icon = '💚'}; + messageElement.innerText =`${icon}${blocked.SenderName}: ${blocked.text}`; + chatWindow.appendChild(messageElement); + chatWindow.scrollTop = chatWindow.scrollHeight; + }); + + + + + socket.on('logout', () => { + quitChat(socket); + }); + + socket.on('privMessageCopy', (message) => { + addMessage(message); + }) + + //receives broadcast of the next GAME + socket.on('nextGame', (message: string) => { + openMessagePopup(message); + // addMessage(message); + }) + + let toggle = false + window.addEventListener("focus", async () => { + //nst bwhoami = document.getElementById('b-whoami') as HTMLButtonElement; + + setTimeout(() => { + connected(socket); + }, 0); + if (window.location.pathname === '/app/chat') { + console.log('%cWindow is focused on /chat:' + socket.id, color.green); + if (socket.id) { + await windowStateVisable(); + } + toggle = true; + } + }); + + window.addEventListener("blur", () => { + console.log('%cWindow is not focused on /chat', color.red); + if (socket.id) + windowStateHidden(); + toggle = false; + }); + + // setInterval(async () => { + // //connected(socket); + // },10000); // every 10 sec + + socket.on('listBud', async (myBuddies: string) => { + const buddies = document.getElementById('div-buddies') as HTMLDivElement; + console.log('%cList buddies connected ',color.yellow, myBuddies); + listBuddies(socket, buddies, myBuddies); + }); + + socket.once('welcome', (data) => { + const buddies = document.getElementById('div-buddies') as HTMLDivElement; + const chatWindow = document.getElementById('t-chatbox') as HTMLDivElement; + chatWindow.innerHTML = ''; + buddies.textContent = ''; + buddies.innerHTML = ''; + connected(socket); + addMessage (`${data.msg} ` + getUser()?.name); + }); + + + setTitle('Chat Page'); + // Listen for the 'connect' event + return { + + html: authHtml, postInsert: async (app) => { + const sendButton = document.getElementById('b-send') as HTMLButtonElement; + const chatWindow = document.getElementById('t-chatbox') as HTMLDivElement; + const sendtextbox = document.getElementById('t-chat-window') as HTMLButtonElement; + const clearText = document.getElementById('b-clear') as HTMLButtonElement; + const bwhoami = document.getElementById('b-whoami') as HTMLButtonElement; + const bconnected = document.getElementById('b-help') as HTMLButtonElement; + const username = document.getElementById('username') as HTMLDivElement; + const buddies = document.getElementById('div-buddies') as HTMLDivElement; + const bquit = document.getElementById('b-quit') as HTMLDivElement; + const systemWindow = document.getElementById('system-box') as HTMLDivElement; + const bnextGame = document.getElementById('b-nextGame') as HTMLDivElement; + + chatWindow.textContent = ''; + chatWindow.innerHTML = ''; + buddies.textContent = ''; + buddies.innerHTML = ''; + + const buttonPro = document.getElementById("close-modal") ?? null; + + if (buttonPro) + buttonPro.addEventListener("click", () => { + const profilList = document.getElementById("profile-modal") ?? null; + if (profilList) profilList.classList.add("hidden"); + }); + + + + + const buttonMessage = document.getElementById("close-modal-message") ?? null; + + if (buttonMessage) + buttonMessage.addEventListener("click", () => { + const gameMessage = document.getElementById("game-modal") ?? null; + if (gameMessage) gameMessage.classList.add("hidden"); + const modalmessage = document.getElementById("modal-message") ?? null; + if (modalmessage) {modalmessage.innerHTML = "";} + + }); + + + // Send button + sendButton?.addEventListener("click", () => { + if (sendtextbox && sendtextbox.value.trim()) { + let msgText: string = sendtextbox.value.trim(); + const msgCommand = parseCmdMsg(msgText) ?? ""; + connected(socket); + if (msgCommand !== "") { + switch (msgCommand[0]) { + case '@msg': + broadcastMsg(socket, msgCommand); + break; + case '@who': + whoami(socket); + break; + case '@profil': + getProfil(socket, msgCommand[1]); + break; + case '@cls': + chatWindow.innerHTML = ''; + break; + case '@quit': + quitChat(socket); + break; + default: + const user = getUser()?.name; + // Ensure we have a user AND socket is connected + if (!user || !socket.connected) return; + const message = { + command: msgCommand[0], + destination: '', + type: "chat", + user: user, + token: document.cookie ?? "", + text: msgCommand[1], + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + //socket.emit('MsgObjectServer', message); + socket.emit('privMessage', JSON.stringify(message)); + // addMessage(JSON.stringify(message)); + break; + } + // Clear the input in all cases + sendtextbox.value = ""; + } + } + }); + + // Clear Text button + clearText?.addEventListener("click", () => { + if (chatWindow) { + chatWindow.innerHTML = ''; + } + //clearChatWindow(socket); //DEV testing broadcastGames + }); + + // Dev Game message button + bnextGame?.addEventListener("click", () => { + if (chatWindow) { + socket.emit('nextGame'); + } + }); + + bquit?.addEventListener('click', () => { + quitChat(socket); + }); + + // Enter key to send message + sendtextbox!.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + sendButton?.click(); + } + }); + + // Whoami button to display user name addMessage(msgCommand[0]); + + bwhoami?.addEventListener('click', async () => { + whoami(socket); + }); + } + } +}; +addRoute('/pong', handleChat, { bypass_auth: true }); \ No newline at end of file diff --git a/frontend/src/pages/pong/types_front.ts b/frontend/src/pages/pong/types_front.ts new file mode 100644 index 0000000..807d071 --- /dev/null +++ b/frontend/src/pages/pong/types_front.ts @@ -0,0 +1,23 @@ +export type ClientMessage = { + command: string + destination: string; + user: string; + text: string; + SenderWindowID: string; +}; + + +export type ClientProfil = { + command: string, + destination: string, + type: string, + user: string, + loginName: string, + userID: string, + text: string, + timestamp: number, + SenderWindowID:string, + SenderName: string, + Sendertext: string, + innerHtml?: string, +}; \ No newline at end of file diff --git a/frontend/src/pages/pong/windowStateHidden.ts b/frontend/src/pages/pong/windowStateHidden.ts new file mode 100644 index 0000000..27c880b --- /dev/null +++ b/frontend/src/pages/pong/windowStateHidden.ts @@ -0,0 +1,18 @@ +import { __socket } from './pong'; +import { updateUser } from "@app/auth"; + +export async function windowStateHidden() { + const socketId = __socket || undefined; + // let oldName = localStorage.getItem("oldName") ?? undefined; + let oldName: string; + if (socketId === undefined) return; + let userName = await updateUser(); + oldName = userName?.name ?? ""; + if (oldName === "") return; + localStorage.setItem('oldName', oldName); + socketId.emit('client_left', { + user: userName?.name, + why: 'tab window hidden - socket not dead', + }); + return; +}; \ No newline at end of file diff --git a/nginx/conf/locations/pong.conf b/nginx/conf/locations/pong.conf new file mode 100644 index 0000000..870619f --- /dev/null +++ b/nginx/conf/locations/pong.conf @@ -0,0 +1,14 @@ +#forward the post request to the microservice +location /api/pong/ { + proxy_pass http://pong; +} + +location /api/pong/socket.io/ { + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 3600s; + proxy_pass http://pong; +} diff --git a/src/pong/.dockerignore b/src/pong/.dockerignore new file mode 100644 index 0000000..c925c21 --- /dev/null +++ b/src/pong/.dockerignore @@ -0,0 +1,2 @@ +/dist +/node_modules diff --git a/src/pong/README.md b/src/pong/README.md new file mode 100644 index 0000000..ceb043c --- /dev/null +++ b/src/pong/README.md @@ -0,0 +1,8 @@ +# Nginx Configuration + +You want to have a new microservice ? + +Edit/add a file in `conf/locations/` +take example on `conf/locations/icons.conf` on how to make a reverse proxy and on how to serve static files + +# Good Luck Have Fun diff --git a/src/pong/entrypoint.sh b/src/pong/entrypoint.sh new file mode 100644 index 0000000..2dcab02 --- /dev/null +++ b/src/pong/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e +set -x +# do anything here + +# run the CMD [ ... ] from the dockerfile +exec "$@" diff --git a/src/pong/extra/.gitkeep b/src/pong/extra/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/pong/openapi.json b/src/pong/openapi.json new file mode 100644 index 0000000..38cd725 --- /dev/null +++ b/src/pong/openapi.json @@ -0,0 +1,21 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "9.6.1", + "title": "@fastify/swagger" + }, + "components": { + "schemas": {} + }, + "paths": {}, + "servers": [ + { + "url": "https://local.maix.me:8888", + "description": "direct from docker" + }, + { + "url": "https://local.maix.me:8000", + "description": "using fnginx" + } + ] +} diff --git a/src/pong/package.json b/src/pong/package.json new file mode 100644 index 0000000..a3fd0e8 --- /dev/null +++ b/src/pong/package.json @@ -0,0 +1,38 @@ +{ + "type": "module", + "private": false, + "name": "pong", + "version": "1.0.0", + "description": "This project was bootstrapped with Fastify-CLI.", + "main": "app.ts", + "directories": { + "test": "test" + }, + "scripts": { + "start": "npm run build && node dist/run.js", + "build": "vite build", + "build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false", + "build:openapi": "VITE_ENTRYPOINT=src/openapi.ts vite build && node dist/openapi.cjs >openapi.json" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@fastify/autoload": "^6.3.1", + "@fastify/formbody": "^8.0.2", + "@fastify/multipart": "^9.3.0", + "@fastify/sensible": "^6.0.4", + "@fastify/static": "^8.3.0", + "@fastify/websocket": "^11.2.0", + "fastify": "^5.6.2", + "fastify-plugin": "^5.1.0", + "socket.io": "^4.8.1", + "typebox": "^1.0.62" + }, + "devDependencies": { + "@types/node": "^22.19.2", + "rollup-plugin-node-externals": "^8.1.2", + "vite": "^7.2.7", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/src/pong/src/app.ts b/src/pong/src/app.ts new file mode 100644 index 0000000..bdf10d0 --- /dev/null +++ b/src/pong/src/app.ts @@ -0,0 +1,441 @@ +import { FastifyInstance, FastifyPluginAsync } from 'fastify'; +import fastifyFormBody from '@fastify/formbody'; +import fastifyMultipart from '@fastify/multipart'; +import * as db from '@shared/database'; +import * as auth from '@shared/auth'; +import * as swagger from '@shared/swagger'; +import * as utils from '@shared/utils'; +import { Server, Socket } from 'socket.io'; +import type { User } from '@shared/database/mixin/user'; +import type { BlockedData } from '@shared/database/mixin/blocked'; +import { broadcast } from './broadcast'; +import type { ClientProfil, ClientMessage } from './chat_types'; +import { sendPrivMessage } from './sendPrivMessage'; +import { sendBlocked } from './sendBlocked'; +import { sendInvite } from './sendInvite'; +import { getUserByName } from './getUserByName'; +import { makeProfil } from './makeProfil'; +import { isBlocked } from './isBlocked'; +import { sendProfil } from './sendProfil'; +import { setGameLink } from './setGameLink'; +import { nextGame_SocketListener } from './nextGame_SocketListener'; +import { list_SocketListener } from './list_SocketListener'; + +// colors for console.log +export const color = { + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + reset: '\x1b[0m', +}; + + +declare const __SERVICE_NAME: string; + +// Global map of clients +// key = socket, value = clientname +interface ClientInfo { + user: string; + lastSeen: number; +} + + + + + +function setAboutPlayer(about: string): string { + if (!about) { + about = 'Player is good Shape - This is a default description'; + } + return about; +}; + +// function setGameLink(link: string): string { +// if (!link) { +// link = 'Click me'; +// } +// return link; +// }; + +export const clientChat = new Map(); + +// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this... +const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true }); +// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this... +const routes = import.meta.glob('./routes/**/*.ts', { eager: true }); + +const app: FastifyPluginAsync = async (fastify, opts): Promise => { + void opts; + + await fastify.register(utils.useMonitoring); + await fastify.register(utils.useMakeResponse); + await fastify.register(swagger.useSwagger, { service: __SERVICE_NAME }); + await fastify.register(db.useDatabase as FastifyPluginAsync, {}); + await fastify.register(auth.jwtPlugin as FastifyPluginAsync, {}); + await fastify.register(auth.authPlugin as FastifyPluginAsync, {}); + + // Place here your custom code! + for (const plugin of Object.values(plugins)) { + void fastify.register(plugin as FastifyPluginAsync, {}); + } + for (const route of Object.values(routes)) { + void fastify.register(route as FastifyPluginAsync, {}); + } + + void fastify.register(fastifyFormBody, {}); + void fastify.register(fastifyMultipart, {}); + + fastify.ready((err) => { + if (err) throw err; + onReady(fastify); + }); +}; +export default app; +export { app }; + +// When using .decorate you have to specify added properties for Typescript +declare module 'fastify' { + interface FastifyInstance { + io: Server<{ + hello: (message: string) => string; + MsgObjectServer: (data: { message: ClientMessage }) => void; + privMessage: (data: string) => void; + profilMessage: (data: ClientProfil) => void; + inviteGame: (data: ClientProfil) => void; + blockUser: (data: ClientProfil) => void; + privMessageCopy: (msg: string) => void; + nextGame: (nextGame: string) => void; + message: (msg: string) => void; + listBud: (msg: string) => void; + client_entered: (userName: string, user: string) => void; + client_left: (userName: string, why: string) => void; + list: (oldUser: string, user: string) => void; + updateClientName: (oldUser: string, user: string) => void; + }>; + } +} + +async function onReady(fastify: FastifyInstance) { + + + // shows address for connection au server transcendance + const session = process.env.SESSION_MANAGER ?? ''; + if (session) { + const part = session.split('/')[1]; + const machineName = part.split('.')[0]; + console.log(color.yellow, 'Connect at : https://' + machineName + ':8888/app/login'); + } + + + fastify.io.on('connection', (socket: Socket) => { + + socket.on('message', (message: string) => { + //console.info(color.blue, 'DEBUG LOG: Socket connected!', color.reset, socket.id); + // console.log( color.blue, 'DEBUG LOG: Received message from client', color.reset, message); + const obj: ClientMessage = JSON.parse(message) as ClientMessage; + clientChat.set(socket.id, { user: obj.user, lastSeen: Date.now() }); + // console.log(color.green, 'DEBUG LOG: Message from client', color.reset, `Sender: login name: ${obj.user} - windowID ${obj.SenderWindowID} - text message: ${obj.text}`); + socket.emit('welcome', {msg: 'Welcome to the chat! : '}); + // Send object directly — DO NOT wrap it in a string + broadcast(fastify, obj, obj.SenderWindowID); + // console.log(color.red, 'DEBUG LOG: connected in the Chat :', connectedUser(fastify.io), color.reset); + }); + + nextGame_SocketListener(fastify, socket); + + list_SocketListener(fastify, socket); + + // socket.on('list', (object) => { + + // const userFromFrontend = object || null; + // const client = clientChat.get(socket.id) || null; + + // //console.log(color.red, 'DEBUG LOG: list activated', userFromFrontend, color.reset, socket.id); + + // if (userFromFrontend.oldUser !== userFromFrontend.user) { + // //console.log(color.red, 'DEBUG LOG: list activated', userFromFrontend.oldUser, color.reset); + // // if (client?.user === null) { + // // console.log('ERROR: clientName is NULL'); + // // return; + // // }; + // if (client) { + // client.user = userFromFrontend.user; + // } + // } + // connectedUser(fastify.io, socket.id); + // }); + + socket.on('updateClientName', (object) => { + const userFromFrontend = object || null; + const client = clientChat.get(socket.id) || null; + // console.log(color.red, 'DEBUG LOG: whoAMi activated', userFromFrontend, color.reset, socket.id); + if (userFromFrontend.oldUser !== userFromFrontend.user) { + // console.log(color.red, 'DEBUG LOG: whoAMi activated', userFromFrontend.oldUser, color.reset); + // if (client === null) { + // console.log('ERROR: clientName is NULL'); + // return; + // }; + if (client) { + client.user = userFromFrontend.user; + console.log(color.green, `'DEBUG LOG: client.user is, '${client.user}'`); + } + } + }); + + socket.on('logout', () => { + const clientInfo = clientChat.get(socket.id); + const clientName = clientInfo?.user; + + if (!clientName) return; + console.log(color.green, `Client logging out: ${clientName} (${socket.id})`); + const obj = { + command: '', + destination: 'system-info', + type: 'chat' as const, + user: clientName, + token: '', + text: 'LEFT the chat', + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + broadcast(fastify, obj, socket.id); + // Optional: remove from map + clientChat.delete(socket.id); + // Ensure socket is fully disconnected + if (socket.connected) socket.disconnect(true); + }); + + socket.on('disconnecting', (reason) => { + const clientName = clientChat.get(socket.id)?.user || null; + console.log( + color.green, + `Client disconnecting: ${clientName} (${socket.id}) reason:`, + reason, + ); + if (reason === 'transport error') return; + + if (clientName !== null) { + const obj = { + command: '', + destination: 'system-info', + type: 'chat', + user: clientName, + token: '', + text: 'LEFT the chat', + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + + broadcast(fastify, obj, obj.SenderWindowID); + } + }); + + socket.on('client_left', (data) => { + const clientName = clientChat.get(socket.id)?.user || null; + const leftChat = data || null; + console.log( + color.green, + `Left the Chat User: ${clientName} id Socket: ${socket.id} reason:`, + leftChat.why, + ); + + if (clientName !== null) { + const obj = { + command: '', + destination: 'system-info', + type: 'chat', + user: clientName, + token: '', + text: 'LEFT the chat but the window is still open', + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + //console.log(color.blue, 'DEBUG LOG: BROADCASTS OUT :', obj.SenderWindowID); + + broadcast(fastify, obj, obj.SenderWindowID); + // clientChat.delete(obj.user); + } + }); + + + socket.on('privMessage', (data) => { + const clientName: string = clientChat.get(socket.id)?.user || ''; + const prvMessage: ClientMessage = JSON.parse(data) || ''; + console.log( + color.blue, + `DEBUG LOG: ClientName: '${clientName}' id Socket: '${socket.id}' target Name:`, + prvMessage.command, + ); + + if (clientName !== null) { + const obj = { + command: prvMessage.command, + destination: 'privateMsg', + type: 'chat', + user: clientName, + token: '', + text: prvMessage.text, + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + // console.log(color.blue, 'DEBUG LOG: PRIV MESSAGE OUT :', obj.SenderWindowID); + sendPrivMessage(fastify, obj, obj.SenderWindowID); + // clientChat.delete(obj.user); + } + }); + + socket.on('profilMessage', async (data: string) => { + const clientName: string = clientChat.get(socket.id)?.user || ''; + const profilMessage: ClientMessage = JSON.parse(data) || ''; + const users: User[] = fastify.db.getAllUsers() ?? []; + // console.log(color.yellow, 'DEBUG LOG: ALL USERS EVER CONNECTED:', users); + // console.log(color.blue, `DEBUG LOG: ClientName: '${clientName}' id Socket: '${socket.id}' target profil:`, profilMessage.user); + const profile: ClientProfil = await makeProfil(fastify, profilMessage.user, socket); + if (clientName !== null) { + const testuser: User | null = getUserByName(users, profilMessage.user); + console.log(color.yellow, 'user:', testuser?.name ?? 'Guest'); + console.log(color.blue, 'DEBUG - profil message MESSAGE OUT :', profile.SenderWindowID); + sendProfil(fastify, profile, profile.SenderWindowID); + // clientChat.delete(obj.user); + } + }); + + socket.on('inviteGame', async (data: string) => { + const clientName: string = clientChat.get(socket.id)?.user || ''; + const profilInvite: ClientProfil = JSON.parse(data) || ''; + // const users: User[] = fastify.db.getAllUsers() ?? []; + + const inviteHtml: string = 'invites you to a game ' + setGameLink(''); + if (clientName !== null) { + // const testuser: User | null = getUserByName(users, profilInvite.user ?? ''); + // console.log(color.yellow, 'user:', testuser?.name ?? 'Guest'); + sendInvite(fastify, inviteHtml, profilInvite); + } + }); + + + + socket.on('blockUser', async (data: string) => { + const clientName: string = clientChat.get(socket.id)?.user || ''; + const profilBlock: ClientProfil = JSON.parse(data) || ''; + const users: User[] = fastify.db.getAllUsers() ?? []; + const UserToBlock: User | null = getUserByName(users, `${profilBlock.user}`); + const UserAskingToBlock: User | null = getUserByName(users, `${profilBlock.SenderName}`); + + console.log(color.yellow, `user to block: ${profilBlock.user}`); + console.log(color.yellow, UserToBlock); + console.log(color.yellow, `user Asking to block: ${profilBlock.SenderName}`); + console.log(color.yellow, UserAskingToBlock); + + const usersBlocked: BlockedData[] = fastify.db.getAllBlockedUsers() ?? []; + if (!UserAskingToBlock || !UserToBlock || !usersBlocked) return; + const userAreBlocked: boolean = isBlocked(UserAskingToBlock, UserToBlock, usersBlocked); + + if (userAreBlocked) { + console.log(color.green, 'Both users are blocked as requested'); + // return true; // or any other action you need to take + + + console.log(color.red, "ALL BLOCKED USERS:", usersBlocked); + fastify.db.removeBlockedUserFor(UserAskingToBlock!.id, UserToBlock!.id); + const usersBlocked2 = fastify.db.getAllBlockedUsers(); + console.log(color.green, 'remove ALL BLOCKED USERS:', usersBlocked2); + if (clientName !== null) { + const blockedMessage = `'I have un-blocked you'`; + if (clientName !== null) { + const obj = { + command: 'message', + destination: 'privateMsg', + type: 'chat', + user: clientName, + token: '', + text: '', + timestamp: Date.now(), + SenderWindowID: socket.id, + Sendertext: 'You have un-blocked', + }; + // console.log(color.blue, 'DEBUG LOG: PRIV MESSAGE OUT :', obj.SenderWindowID); + socket.emit('privMessageCopy', `${obj.Sendertext}: ${UserToBlock.name}💚`); + // clientChat.delete(obj.user); + } + // profilBlock.Sendertext = `'You have un-blocked '`; + sendBlocked(fastify, blockedMessage, profilBlock); + } + } else { + console.log(color.red, 'The users are not blocked in this way'); + console.log(color.red, "ALL BLOCKED USERS:", usersBlocked); + fastify.db.addBlockedUserFor(UserAskingToBlock!.id, UserToBlock!.id); + const usersBlocked2 = fastify.db.getAllBlockedUsers(); + console.log(color.green, 'ALL BLOCKED USERS:', usersBlocked2); + if (clientName !== null) { + const blockedMessage = `'I have blocked you'`; + profilBlock.Sendertext = `'You have blocked '`; + if (clientName !== null) { + const obj = { + command: 'message', + destination: 'privateMsg', + type: 'chat', + user: clientName, + token: '', + text: '', + timestamp: Date.now(), + SenderWindowID: socket.id, + Sendertext: 'You have blocked', + }; + // console.log(color.blue, 'DEBUG LOG: PRIV MESSAGE OUT :', obj.SenderWindowID); + socket.emit('privMessageCopy', `${obj.Sendertext}: ${UserToBlock.name}⛔`); + // clientChat.delete(obj.user); + } + sendBlocked(fastify, blockedMessage, profilBlock); + } + } + }); + socket.on('client_entered', (data) => { + + // data may be undefined (when frontend calls emit with no payload) + const userNameFromFrontend = data?.userName || null; + const userFromFrontend = data?.user || null; + let clientName = clientChat.get(socket.id)?.user || null; + // const client = clientChat.get(socket.id) || null; + let text = 'is back in the chat'; + + if (clientName === null) { + console.log('ERROR: clientName is NULL'); return; + }; + // if (client === null) { + // console.log('ERROR: client is NULL'); return; + // }; + if (userNameFromFrontend !== userFromFrontend) { + text = `'is back in the chat, I used to be called '${userNameFromFrontend}`; + clientName = userFromFrontend; + if (clientName === null) { + console.log('ERROR: clientName is NULL'); return; + }; + // if (client) { + // client.user = clientName; + // } + } + console.log( + color.green, + `Client entered the Chat: ${clientName} (${socket.id})`, + ); + if (clientName !== null) { + const obj = { + command: '', + destination: 'system-info', + type: 'chat', + user: clientName, + frontendUserName: userNameFromFrontend, + frontendUser: userFromFrontend, + token: '', + text: text, + timestamp: Date.now(), + SenderWindowID: socket.id, + }; + broadcast(fastify, obj, obj.SenderWindowID); + } + }); + + }); +} diff --git a/src/pong/src/broadcast.ts b/src/pong/src/broadcast.ts new file mode 100644 index 0000000..ad04321 --- /dev/null +++ b/src/pong/src/broadcast.ts @@ -0,0 +1,22 @@ +import type { ClientMessage } from './chat_types'; +import { clientChat, color } from './app'; +import { FastifyInstance } from 'fastify'; + +export function broadcast(fastify: FastifyInstance, data: ClientMessage, sender?: string) { + fastify.io.fetchSockets().then((sockets) => { + for (const socket of sockets) { + // Skip sender's own socket + if (socket.id === sender) continue; + // Get client name from map + const clientInfo = clientChat.get(socket.id); + if (!clientInfo?.user) { + console.log(color.yellow, `Skipping socket ${socket.id} (no user found)`); + continue; + } + // Emit structured JSON object + socket.emit('MsgObjectServer', { message: data }); + // Debug logs + // console.log(color.green, `'DEBUG LOG: Broadcast to:', ${data.command} message: ${data.text}`); + } + }); +} \ No newline at end of file diff --git a/src/pong/src/broadcastNextGame.ts b/src/pong/src/broadcastNextGame.ts new file mode 100644 index 0000000..dd2808e --- /dev/null +++ b/src/pong/src/broadcastNextGame.ts @@ -0,0 +1,25 @@ +import { FastifyInstance } from 'fastify'; +import { clientChat, color } from './app'; + +/** + * function broadcast a clickable link + * @param fastify + * @param gameLink + */ +export async function broadcastNextGame(fastify: FastifyInstance, gameLink?: Promise) { + const link = gameLink ? await gameLink : undefined; + console.log(color.green, 'link===========> ', link); + const sockets = await fastify.io.fetchSockets(); + // fastify.io.fetchSockets().then((sockets) => { + for (const socket of sockets) { + const clientInfo = clientChat.get(socket.id); + if (!clientInfo?.user) { + console.log(color.yellow, `DEBUG LOG: Skipping socket ${socket.id} (no user found)`); + continue; + } + if (link) { + socket.emit('nextGame', link); + } + // console.log(color.green, `'DEBUG LOG: Broadcast to:', ${data.command} message: ${data.text}`); + } +}; \ No newline at end of file diff --git a/src/pong/src/chat_types.ts b/src/pong/src/chat_types.ts new file mode 100644 index 0000000..5ed14f9 --- /dev/null +++ b/src/pong/src/chat_types.ts @@ -0,0 +1,23 @@ +export type ClientMessage = { + command: string + destination: string; + user: string; + text: string; + SenderWindowID: string; +}; + +export type ClientProfil = { + command: string, + destination: string, + type: string, + user: string, + loginName: string, + userID: string, + text: string, + timestamp: number, + SenderWindowID:string, + SenderName: string, + Sendertext: string, + innerHtml?: string, + +}; \ No newline at end of file diff --git a/src/pong/src/connectedUser.ts b/src/pong/src/connectedUser.ts new file mode 100644 index 0000000..010d194 --- /dev/null +++ b/src/pong/src/connectedUser.ts @@ -0,0 +1,48 @@ +import { clientChat } from './app'; +import { Server, Socket } from 'socket.io'; + +/** + * function check users connected to the chat with a socket and makes a seen list + * calls listBud socket listener to update Ping Buddies List and calls listBuddies() + * @param io + * @param target + * @returns the number connected + */ + +export function connectedUser(io?: Server, target?: string): number { + let count = 0; + const seen = new Set(); + // <- only log/count unique usernames + for (const [socketId, username] of clientChat) { + // Basic checks + if (typeof socketId !== 'string' || socketId.length === 0) { + clientChat.delete(socketId); + continue; + } + if (typeof username.user !== 'string' || username.user.length === 0) { + clientChat.delete(socketId); + continue; + } + // If we have the io instance, attempt to validate the socket is still connected + if (io && typeof io.sockets?.sockets?.get === 'function') { + const socket = io.sockets.sockets.get(socketId) as Socket | undefined; + // If socket not found or disconnected, remove from map and skip + if (!socket || socket.disconnected) { + clientChat.delete(socketId); + continue; + } + // Skip duplicates (DO NOT delete them — just don't count) + if (seen.has(username.user)) { + continue; + } + // socket exists and is connected + seen.add(username.user); + count++; + const targetSocketId = target; + io.to(targetSocketId!).emit('listBud', username.user); + continue; + } + count++; + } + return count; +} \ No newline at end of file diff --git a/src/pong/src/createNextGame.ts b/src/pong/src/createNextGame.ts new file mode 100644 index 0000000..c8c1115 --- /dev/null +++ b/src/pong/src/createNextGame.ts @@ -0,0 +1,6 @@ +/** +/* TODO find the description info for profil / or profil game link and return +**/ +export function createNextGame() { + return 'The next Game is Starting click here to watch'; +}; diff --git a/src/pong/src/getUserByName.ts b/src/pong/src/getUserByName.ts new file mode 100644 index 0000000..d6b98e4 --- /dev/null +++ b/src/pong/src/getUserByName.ts @@ -0,0 +1,12 @@ + import type { User } from '@shared/database/mixin/user'; + + /** + * function get the object user in an array of users[] by name + * @param users + * @param name + * @returns + */ + + export function getUserByName(users: User[], name: string) { + return users.find(user => user.name === name) || null; + } \ No newline at end of file diff --git a/src/pong/src/isBlocked.ts b/src/pong/src/isBlocked.ts new file mode 100644 index 0000000..42680df --- /dev/null +++ b/src/pong/src/isBlocked.ts @@ -0,0 +1,17 @@ +import type { User } from '@shared/database/mixin/user'; +import type { BlockedData } from '@shared/database/mixin/blocked'; + +/** + * function compares the four ids of two users and returns true if + * UserA1 = UserB1 and UserB1 = UserB2 + * @param UserAskingToBlock + * @param UserToBlock + * @param usersBlocked + * @returns + */ + +export function isBlocked(UserAskingToBlock: User, UserToBlock: User, usersBlocked: BlockedData[]): boolean { + return usersBlocked.some(blocked => + blocked.blocked === UserToBlock?.id && + blocked.user === UserAskingToBlock?.id); +} \ No newline at end of file diff --git a/src/pong/src/list_SocketListener.ts b/src/pong/src/list_SocketListener.ts new file mode 100644 index 0000000..9ec056f --- /dev/null +++ b/src/pong/src/list_SocketListener.ts @@ -0,0 +1,29 @@ +import type { FastifyInstance } from 'fastify'; +import { Socket } from 'socket.io'; +import { clientChat } from './app'; +import { connectedUser } from './connectedUser'; +// import { color } from './app'; + + + +export function list_SocketListener(fastify: FastifyInstance, socket: Socket) { + + socket.on('list', (object) => { + + const userFromFrontend = object || null; + const client = clientChat.get(socket.id) || null + //console.log(color.red, 'DEBUG LOG: list activated', userFromFrontend, color.reset, socket.id) + if (userFromFrontend.oldUser !== userFromFrontend.user) { + //console.log(color.red, 'DEBUG LOG: list activated', userFromFrontend.oldUser, color.reset); + if (client?.user === null) { + console.log('ERROR: clientName is NULL'); + return; + }; + if (client) { + client.user = userFromFrontend.user; + } + } + connectedUser(fastify.io, socket.id); + }); + +} \ No newline at end of file diff --git a/src/pong/src/makeProfil.ts b/src/pong/src/makeProfil.ts new file mode 100644 index 0000000..c45e658 --- /dev/null +++ b/src/pong/src/makeProfil.ts @@ -0,0 +1,41 @@ +import { FastifyInstance } from 'fastify'; +import type { ClientProfil } from './chat_types'; +import type { User } from '@shared/database/mixin/user'; +import { getUserByName } from './getUserByName'; +import { Socket } from 'socket.io'; + +/** + * function makeProfil - translates the Users[] to a one user looking by name + * and puts it into ClientProfil format + * @param fastify + * @param user + * @param socket + * @returns + */ + +export async function makeProfil(fastify: FastifyInstance, user: string, socket: Socket): Promise { + + let clientProfil!: ClientProfil; + const users: User[] = fastify.db.getAllUsers() ?? []; + const allUsers: User | null = getUserByName(users, user); + // console.log(color.yellow, `DEBUG LOG: 'userFound is:'${allUsers?.name}`); + if (user === allUsers?.name) { + // console.log(color.yellow, `DEBUG LOG: 'login Name: '${allUsers.login}' user: '${user}'`); + clientProfil = + { + command: 'makeProfil', + destination: 'profilMsg', + type: 'chat' as const, + user: `${allUsers.name}`, + loginName: `${allUsers?.login ?? 'Guest'}`, + userID: `${allUsers?.id ?? ''}`, + text: '', + timestamp: Date.now(), + SenderWindowID: socket.id, + SenderName: '', + Sendertext: '', + innerHtml: '', + }; + } + return clientProfil; +}; \ No newline at end of file diff --git a/src/pong/src/nextGame_SocketListener.ts b/src/pong/src/nextGame_SocketListener.ts new file mode 100644 index 0000000..b30095f --- /dev/null +++ b/src/pong/src/nextGame_SocketListener.ts @@ -0,0 +1,20 @@ +import type { FastifyInstance } from 'fastify'; +import { broadcastNextGame } from './broadcastNextGame'; +import { Socket } from 'socket.io'; +import { createNextGame } from './createNextGame'; +import { sendGameLinkToChatService } from './sendGameLinkToChatService'; + +/** + * function listens to the socket for a nextGame emit + * once triggered it broadcasts the pop up + * TODO plug this into backend of the game Chat + * @param fastify + * @param socket + */ +export function nextGame_SocketListener(fastify: FastifyInstance, socket: Socket) { + socket.on('nextGame', () => { + const link = createNextGame(); + const game: Promise = sendGameLinkToChatService(link); + broadcastNextGame(fastify, game); + }); +} \ No newline at end of file diff --git a/src/pong/src/openapi.ts b/src/pong/src/openapi.ts new file mode 100644 index 0000000..d66d7a7 --- /dev/null +++ b/src/pong/src/openapi.ts @@ -0,0 +1,21 @@ +import f, { FastifyPluginAsync } from 'fastify'; +import * as swagger from '@shared/swagger'; +import * as auth from '@shared/auth'; + +declare const __SERVICE_NAME: string; + +// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this... +const routes = import.meta.glob('./routes/**/*.ts', { eager: true }); + +async function start() { + const fastify = f({ logger: false }); + await fastify.register(auth.authPlugin, { onlySchema: true }); + await fastify.register(swagger.useSwagger, { service: __SERVICE_NAME }); + + for (const route of Object.values(routes)) { + await fastify.register(route as FastifyPluginAsync, {}); + } + await fastify.ready(); + console.log(JSON.stringify(fastify.swagger(), undefined, 4)); +} +start(); diff --git a/src/pong/src/plugins/README.md b/src/pong/src/plugins/README.md new file mode 100644 index 0000000..1e61ee5 --- /dev/null +++ b/src/pong/src/plugins/README.md @@ -0,0 +1,16 @@ +# Plugins Folder + +Plugins define behavior that is common to all the routes in your +application. Authentication, caching, templates, and all the other cross +cutting concerns should be handled by plugins placed in this folder. + +Files in this folder are typically defined through the +[`fastify-plugin`](https://github.com/fastify/fastify-plugin) module, +making them non-encapsulated. They can define decorators and set hooks +that will then be used in the rest of your application. + +Check out: + +* [The hitchhiker's guide to plugins](https://fastify.dev/docs/latest/Guides/Plugins-Guide/) +* [Fastify decorators](https://fastify.dev/docs/latest/Reference/Decorators/). +* [Fastify lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/). diff --git a/src/pong/src/plugins/sensible.ts b/src/pong/src/plugins/sensible.ts new file mode 100644 index 0000000..e324067 --- /dev/null +++ b/src/pong/src/plugins/sensible.ts @@ -0,0 +1,10 @@ +import fp from 'fastify-plugin'; +import sensible, { FastifySensibleOptions } from '@fastify/sensible'; +/** + * This plugins adds some utilities to handle http errors + * + * @see https://github.com/fastify/fastify-sensible + */ +export default fp(async (fastify) => { + fastify.register(sensible); +}); \ No newline at end of file diff --git a/src/pong/src/plugins/socket.ts b/src/pong/src/plugins/socket.ts new file mode 100644 index 0000000..ed248ff --- /dev/null +++ b/src/pong/src/plugins/socket.ts @@ -0,0 +1,31 @@ +import type { + FastifyInstance, + FastifyPluginAsync, + HookHandlerDoneFunction, +} from 'fastify'; +import fp from 'fastify-plugin'; +import { Server } from 'socket.io'; + +const F: ( + f: FastifyInstance, +) => Omit & { io: Server } = (f) => + f as Omit & { io: Server }; + +const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => { + function defaultPreClose(done: HookHandlerDoneFunction) { + F(fastify).io.local.disconnectSockets(true); + done(); + } + fastify.decorate( + 'io', + new Server(fastify.server, { path: '/api/pong/socket.io' }), + ); + fastify.addHook('preClose', defaultPreClose); + fastify.addHook('onClose', (instance: FastifyInstance, done) => { + F(instance).io.close(); + done(); + }); +}); + +export default fastifySocketIO; + diff --git a/src/pong/src/routes/broadcast.ts b/src/pong/src/routes/broadcast.ts new file mode 100644 index 0000000..2b2481c --- /dev/null +++ b/src/pong/src/routes/broadcast.ts @@ -0,0 +1,57 @@ +import { FastifyPluginAsync } from 'fastify'; +import { Static, Type } from 'typebox'; +import { broadcast } from '../broadcast'; + + + +export const PongReq = Type.Object({ + message: Type.String(), +}); + +export type PongReq = Static; + + +const route: FastifyPluginAsync = async (fastify): Promise => { + fastify.post<{ Body: PongReq }>( + '/api/pong/broadcast', + { + schema: { + body: PongReq, + hide: true, + }, + config: { requireAuth: false }, + }, + async function(req, res) { + broadcast(this, { command: '', destination: '', user: 'CMwaLeSever!!', text: req.body.message, SenderWindowID: 'server' }); + void res; + }, + ); +}; +export default route; + + +// const route: FastifyPluginAsync = async (fastify): Promise => { +// fastify.post('/api/chat/broadcast', { +// schema: { +// body: { +// type: 'object', +// required: ['nextGame'], +// properties: { +// nextGame: { type: 'string' } +// } +// } +// } +// }, async (req, reply) => { + +// // Body only contains nextGame now +// const gameLink: Promise = Promise.resolve(req.body as string ); + +// // Broadcast nextGame +// if (gameLink) +// broadcastNextGame(fastify, gameLink); + +// return reply.send({ status: 'ok' }); +// }); +// }; +// export default route; + diff --git a/src/pong/src/run.ts b/src/pong/src/run.ts new file mode 100644 index 0000000..d9f1e2a --- /dev/null +++ b/src/pong/src/run.ts @@ -0,0 +1,36 @@ +// this sould only be used by the docker file ! + +import fastify, { FastifyInstance } from 'fastify'; +import app from './app'; + +const start = async () => { + const envToLogger = { + development: { + transport: { + target: 'pino-pretty', + options: { + translateTime: 'HH:MM:ss Z', + ignore: 'pid,hostname', + }, + }, + }, + production: true, + test: false, + }; + + const f: FastifyInstance = fastify({ logger: envToLogger.development }); + try { + process.on('SIGTERM', () => { + f.log.info('Requested to shutdown'); + process.exit(134); + }); + console.log('-------->Serving static files from:'); + await f.register(app, {}); + await f.listen({ port: 80, host: '0.0.0.0' }); + } + catch (err) { + f.log.error(err); + process.exit(1); + }; +}; +start(); \ No newline at end of file diff --git a/src/pong/src/sendBlocked.ts b/src/pong/src/sendBlocked.ts new file mode 100644 index 0000000..d686e15 --- /dev/null +++ b/src/pong/src/sendBlocked.ts @@ -0,0 +1,29 @@ +import type { ClientProfil } from './chat_types'; +import { clientChat, color } from './app'; +import { FastifyInstance } from 'fastify'; + +/** + * function looks for the online (socket) for user to block, when found send ordre to block or unblock user + * @param fastify + * @param blockedMessage + * @param profil + */ + +export function sendBlocked(fastify: FastifyInstance, blockedMessage: string, profil: ClientProfil) { + fastify.io.fetchSockets().then((sockets) => { + let targetSocket; + for (const socket of sockets) { + const clientInfo: string = clientChat.get(socket.id)?.user || ''; + if (clientInfo === profil.user) { + console.log(color.yellow, 'DEBUG LOG: User found online to block:', profil.user); + targetSocket = socket || ''; + break; + } + } + profil.text = blockedMessage ?? ''; + // console.log(color.red, 'DEBUG LOG:',profil.Sendertext); + if (targetSocket) { + targetSocket.emit('blockUser', profil); + } + }); +} diff --git a/src/pong/src/sendGameLinkToChatService.ts b/src/pong/src/sendGameLinkToChatService.ts new file mode 100644 index 0000000..2705641 --- /dev/null +++ b/src/pong/src/sendGameLinkToChatService.ts @@ -0,0 +1,7 @@ +/** +/* EXPERIMENTAL: how to send a starting game link to chat +**/ +export async function sendGameLinkToChatService(link: string) :Promise { + const payload = { link }; + return JSON.stringify(payload); +} \ No newline at end of file diff --git a/src/pong/src/sendInvite.ts b/src/pong/src/sendInvite.ts new file mode 100644 index 0000000..c58005d --- /dev/null +++ b/src/pong/src/sendInvite.ts @@ -0,0 +1,30 @@ +import type { ClientProfil } from './chat_types'; +import { clientChat, color } from './app'; +import { FastifyInstance } from 'fastify'; + +/** + * function looks for the user online in the chat + * and sends emit to invite - format HTML to make clickable + * message appears in chat window text area + * @param fastify + * @param innerHtml + * @param profil + */ + +export function sendInvite(fastify: FastifyInstance, innerHtml: string, profil: ClientProfil) { + fastify.io.fetchSockets().then((sockets) => { + let targetSocket; + for (const socket of sockets) { + const clientInfo: string = clientChat.get(socket.id)?.user || ''; + if (clientInfo === profil.user) { + console.log(color.yellow, 'DEBUG LOG: user online found', profil.user); + targetSocket = socket || ''; + break; + } + } + profil.innerHtml = innerHtml ?? ''; + if (targetSocket) { + targetSocket.emit('inviteGame', profil); + } + }); +} diff --git a/src/pong/src/sendPrivMessage.ts b/src/pong/src/sendPrivMessage.ts new file mode 100644 index 0000000..c0c362d --- /dev/null +++ b/src/pong/src/sendPrivMessage.ts @@ -0,0 +1,39 @@ +import type { ClientMessage } from './chat_types'; +import { clientChat, color } from './app'; +import { FastifyInstance } from 'fastify'; + +/** + * function looks up the socket of a user online in the chat and sends a message + * it also sends a copy of the message to the sender + * @param fastify + * @param data + * @param sender + */ + +export function sendPrivMessage(fastify: FastifyInstance, data: ClientMessage, sender?: string) { + fastify.io.fetchSockets().then((sockets) => { + const senderSocket = sockets.find(socket => socket.id === sender); + for (const socket of sockets) { + if (socket.id === sender) continue; + const clientInfo = clientChat.get(socket.id); + if (!clientInfo?.user) { + console.log(color.yellow, `DEBUG LOG: Skipping socket ${socket.id} (no user found)`); + continue; + } + const user: string = clientChat.get(socket.id)?.user ?? ''; + const atUser = `@${user}`; + if (atUser !== data.command || atUser === '') { + console.log(color.yellow, `DEBUG LOG: User: '${atUser}' command NOT FOUND: '${data.command[0]}' `); + continue; + } + if (data.text !== '') { + socket.emit('MsgObjectServer', { message: data }); + console.log(color.yellow, `DEBUG LOG: User: '${atUser}' command FOUND: '${data.command}' `); + if (senderSocket) { + senderSocket.emit('privMessageCopy', `${data.command}: ${data.text}🔒`); + } + } + console.log(color.green, `DEBUG LOG: 'Priv to:', ${data.command} message: ${data.text}`); + } + }); +} diff --git a/src/pong/src/sendProfil.ts b/src/pong/src/sendProfil.ts new file mode 100644 index 0000000..4f92718 --- /dev/null +++ b/src/pong/src/sendProfil.ts @@ -0,0 +1,19 @@ +import { FastifyInstance } from 'fastify'; +import type { ClientProfil } from './chat_types'; + +/** + * function takes a user profil and sends it to the asker by window id + * @param fastify + * @param profil + * @param SenderWindowID + */ + +export function sendProfil(fastify: FastifyInstance, profil: ClientProfil, SenderWindowID?: string) { + fastify.io.fetchSockets().then((sockets) => { + const senderSocket = sockets.find(socket => socket.id === SenderWindowID); + if (senderSocket) { + // console.log(color.yellow, 'DEBUG LOG: profil.info:', profil.user); + senderSocket.emit('profilMessage', profil); + } + }); +} \ No newline at end of file diff --git a/src/pong/src/setGameLink.ts b/src/pong/src/setGameLink.ts new file mode 100644 index 0000000..c5067fa --- /dev/null +++ b/src/pong/src/setGameLink.ts @@ -0,0 +1,7 @@ + +export function setGameLink(link: string): string { + if (!link) { + link = 'Click me'; + } + return link; +}; \ No newline at end of file diff --git a/src/pong/tsconfig.json b/src/pong/tsconfig.json new file mode 100644 index 0000000..a731182 --- /dev/null +++ b/src/pong/tsconfig.json @@ -0,0 +1,15 @@ +// { +// "extends": "../tsconfig.base.json", +// "compilerOptions": { +// "skipLibCheck": true, // skips type checking for all .d.ts files +// "moduleResolution": "node", +// "esModuleInterop": true, +// "types": ["node"] }, +// "include": ["src/**/*.ts"] +// } + +{ + "extends": "../tsconfig.base.json", + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} diff --git a/src/pong/vite.config.js b/src/pong/vite.config.js new file mode 100644 index 0000000..b5e78df --- /dev/null +++ b/src/pong/vite.config.js @@ -0,0 +1,53 @@ +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import nodeExternals from 'rollup-plugin-node-externals'; +import path from 'node:path'; +import fs from 'node:fs'; + +function collectDeps(...pkgJsonPaths) { + const allDeps = new Set(); + for (const pkgPath of pkgJsonPaths) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + for (const dep of Object.keys(pkg.dependencies || {})) { + allDeps.add(dep); + } + for (const peer of Object.keys(pkg.peerDependencies || {})) { + allDeps.add(peer); + } + } + return Array.from(allDeps); +}; + +const externals = collectDeps( + './package.json', + '../@shared/package.json', +); + +export default defineConfig({ + root: __dirname, + define: { + __SERVICE_NAME: '"pong"', + }, + // service root + plugins: [tsconfigPaths(), nodeExternals()], + build: { + ssr: true, + outDir: 'dist', + emptyOutDir: true, + lib: { + entry: path.resolve(__dirname, process.env.VITE_ENTRYPOINT ?? 'src/run.ts'), + // adjust main entry + formats: ['cjs'], + // CommonJS for Node.js + fileName: () => 'index.js', + }, + rollupOptions: { + external: externals, + }, + target: 'node22', + // or whatever Node version you use + sourcemap: true, + minify: false, + // for easier debugging + }, +});