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 ...
+
+
+
+
+
+
+
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
+ },
+});