Started Pong simple copy Chat - WIP simplification first job

This commit is contained in:
NigeParis 2025-12-13 07:15:09 +01:00 committed by Maix0
parent c39e7b9e04
commit 7c20066b63
48 changed files with 2080 additions and 0 deletions

2
src/pong/.dockerignore Normal file
View file

@ -0,0 +1,2 @@
/dist
/node_modules

8
src/pong/README.md Normal file
View file

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

8
src/pong/entrypoint.sh Normal file
View file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
set -x
# do anything here
# run the CMD [ ... ] from the dockerfile
exec "$@"

0
src/pong/extra/.gitkeep Normal file
View file

21
src/pong/openapi.json Normal file
View file

@ -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"
}
]
}

38
src/pong/package.json Normal file
View file

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

441
src/pong/src/app.ts Normal file
View file

@ -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 = '<a href=\'https://google.com\' style=\'color: blue; text-decoration: underline; cursor: pointer;\'>Click me</a>';
// }
// return link;
// };
export const clientChat = new Map<string, ClientInfo>();
// @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> => {
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);
}
});
});
}

22
src/pong/src/broadcast.ts Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
/**
/* TODO find the description info for profil / or profil game link and return
**/
export function createNextGame() {
return '<a href=\'https://localhost:8888/app/\' style=\'color: blue; text-decoration: underline; cursor: pointer;\'>The next Game is Starting click here to watch</a>';
};

View file

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

17
src/pong/src/isBlocked.ts Normal file
View file

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

View file

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

View file

@ -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 <ClientProfil> {
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;
};

View file

@ -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<string> = sendGameLinkToChatService(link);
broadcastNextGame(fastify, game);
});
}

21
src/pong/src/openapi.ts Normal file
View file

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

View file

@ -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/).

View file

@ -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<FastifySensibleOptions>(async (fastify) => {
fastify.register(sensible);
});

View file

@ -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<FastifyInstance, 'io'> & { io: Server } = (f) =>
f as Omit<FastifyInstance, 'io'> & { 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;

View file

@ -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<typeof PongReq>;
const route: FastifyPluginAsync = async (fastify): Promise<void> => {
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<void> => {
// 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<string> = Promise.resolve(req.body as string );
// // Broadcast nextGame
// if (gameLink)
// broadcastNextGame(fastify, gameLink);
// return reply.send({ status: 'ok' });
// });
// };
// export default route;

36
src/pong/src/run.ts Normal file
View file

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

View file

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

View file

@ -0,0 +1,7 @@
/**
/* EXPERIMENTAL: how to send a starting game link to chat
**/
export async function sendGameLinkToChatService(link: string) :Promise<string> {
const payload = { link };
return JSON.stringify(payload);
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
export function setGameLink(link: string): string {
if (!link) {
link = '<a href=\'https://google.com\' style=\'color: blue; text-decoration: underline; cursor: pointer;\'>Click me</a>';
}
return link;
};

15
src/pong/tsconfig.json Normal file
View file

@ -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"]
}

53
src/pong/vite.config.js Normal file
View file

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