Nigel/block user (#60)

* WIP block users

* Add test to check if the person for user X is blocked or not

* Added function isBlocked return true if user x is blocked by user y

* Added block ans un block messages

* Route / broadcast added ny Maix

* notification block and un block with icon now works, started clean up

* cleaning up the code

* cleaning up the code - WIP

* cleaning up the code - WIP

* clean up code - WIP

* clean up code - WIP

* WIP block users

* Add test to check if the person for user X is blocked or not

* Added function isBlocked return true if user x is blocked by user y

* Added block ans un block messages

* REBASE -trying

* REBASE trying

* REBASE problems

* REBASE problems

* REBASE problems

* MERGE problems

* REBASE problem

* REBASE problem

* clean up code - WIP

* MERGED master into nigel/blockUser

* linter OK

* TTT problem with package

* Deletes tic tac and icons
This commit is contained in:
Nigel 2025-12-16 15:21:07 +01:00 committed by GitHub
parent 64a820c0f0
commit 85eca5d301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 2613 additions and 1891 deletions

View file

@ -7,116 +7,7 @@
"components": {
"schemas": {}
},
"paths": {
"/api/chat/test": {
"get": {
"operationId": "chatTest",
"responses": {
"200": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"kind",
"msg",
"payload"
],
"properties": {
"kind": {
"enum": [
"success"
]
},
"msg": {
"enum": [
"chat.success"
]
},
"payload": {
"type": "object",
"required": [
"name",
"id",
"guest"
],
"properties": {
"name": {
"type": "string"
},
"id": {
"type": "string"
},
"guest": {
"type": "boolean"
}
}
}
}
}
}
}
},
"401": {
"description": "Default Response",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"notLoggedIn"
]
},
"msg": {
"enum": [
"auth.noCookie",
"auth.invalidKind",
"auth.noUser",
"auth.invalid"
]
}
}
},
{
"type": "object",
"required": [
"kind",
"msg"
],
"properties": {
"kind": {
"enum": [
"notLoggedIn"
]
},
"msg": {
"enum": [
"auth.noCookie",
"auth.invalidKind",
"auth.noUser",
"auth.invalid"
]
}
}
}
]
}
}
}
}
}
}
}
},
"paths": {},
"servers": [
{
"url": "https://local.maix.me:8888",

View file

@ -27,12 +27,12 @@
"fastify": "^5.6.2",
"fastify-plugin": "^5.1.0",
"socket.io": "^4.8.1",
"typebox": "^1.0.61"
"typebox": "^1.0.63"
},
"devDependencies": {
"@types/node": "^22.19.2",
"@types/node": "^22.19.3",
"rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.7",
"vite": "^7.3.0",
"vite-tsconfig-paths": "^5.1.4"
}
}

View file

@ -7,7 +7,19 @@ 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 { sendGameLinkToChatService } from '../../@shared/src/utils/index';
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 = {
@ -18,14 +30,6 @@ export const color = {
reset: '\x1b[0m',
};
// 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');
}
declare const __SERVICE_NAME: string;
// Global map of clients
@ -35,52 +39,21 @@ interface ClientInfo {
lastSeen: number;
}
export type ClientMessage = {
command: string
destination: string;
user: string;
text: string;
SenderWindowID: string;
};
// function setAboutPlayer(about: string): string {
// if (!about) {
// about = 'Player is good Shape - This is a default description';
// }
// return about;
// };
export type ClientProfil = {
command: string,
destination: string,
type: string,
user: string,
loginName: string,
userID: string,
text: string,
timestamp: number,
SenderWindowID:string,
SenderName: string,
innerHtml?: string,
// 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;
// };
};
/**
/* TODO find the description info for profil / or profil game link and return
**/
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>';
};
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;
};
const clientChat = new Map<string, ClientInfo>();
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 });
@ -125,11 +98,11 @@ declare module 'fastify' {
privMessage: (data: string) => void;
profilMessage: (data: ClientProfil) => void;
inviteGame: (data: ClientProfil) => void;
blockUser: (data: ClientProfil) => void;
privMessageCopy: (msg: string) => void;
nextGame: (msg: string) => void;
nextGame: (nextGame: string) => void;
message: (msg: string) => void;
listBud: (msg: string) => void;
testend: (sock_id_client: string) => void;
client_entered: (userName: string, user: string) => void;
client_left: (userName: string, why: string) => void;
list: (oldUser: string, user: string) => void;
@ -139,259 +112,68 @@ declare module 'fastify' {
}
async function onReady(fastify: FastifyInstance) {
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 sanity 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 s = io.sockets.sockets.get(socketId) as Socket | undefined;
// If socket not found or disconnected, remove from map and skip
if (!s || s.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;
// 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');
}
function broadcast(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, `'Broadcast to:', ${data.command} message: ${data.text}`);
}
});
}
async function broadcastNextGame(gameLink?: Promise<string>) {
const link = gameLink ? await gameLink : undefined;
const sockets = await fastify.io.fetchSockets();
// fastify.io.fetchSockets().then((sockets) => {
for (const socket of sockets) {
// Skip sender's own socket
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
if (link) {
socket.emit('nextGame', link);
}
// Debug logs
// console.log(color.green, `'DEBUG LOG: Broadcast to:', ${data.command} message: ${data.text}`);
}
};
// function formatTimestamp(ms: number) {
// const d = new Date(ms);
// return d.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' });
// }
function getUserByName(users: User[], name: string) {
return users.find(user => user.name === name) || null;
}
// this function returns html the profil pop up in CHAT of a user 'nickname unique' TODO ....
async function getProfil(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, `'userFound is:'${allUsers?.name}`);
if (user === allUsers?.name) {
console.log(color.yellow, `'login Name: '${allUsers.login}' user: '${user}'`);
clientProfil =
{
command: 'getProfil',
destination: 'profilMsg',
type: 'chat' as const,
user: `${allUsers.name}`,
loginName: `${allUsers?.login ?? 'Guest'}`,
userID: `${allUsers?.id ?? ''}`,
text: setAboutPlayer(''),
timestamp: Date.now(),
SenderWindowID: socket.id,
SenderName: '',
innerHtml: '',
};
}
return clientProfil;
};
function sendProfil(data: ClientProfil, clientProfil?: string) {
fastify.io.fetchSockets().then((sockets) => {
const senderSocket = sockets.find(socket => socket.id === clientProfil);
if (senderSocket) {
console.log(color.yellow, 'user inFO:', data.user);
senderSocket.emit('profilMessage', data);
}
});
}
function sendInvite(innerHtml: string, data: ClientProfil) {
fastify.io.fetchSockets().then((sockets) => {
let targetSocket;
for (const socket of sockets) {
const clientInfo: string = clientChat.get(socket.id)?.user || '';
if (clientInfo === data.user) {
console.log(color.yellow, 'FOUND:', data.user);
targetSocket = socket || '';
break;
}
}
data.innerHtml = innerHtml ?? '';
if (targetSocket) {
targetSocket.emit('inviteGame', data);
}
});
}
function sendPrivMessage(data: ClientMessage, sender?: string) {
fastify.io.fetchSockets().then((sockets) => {
const senderSocket = sockets.find(s => s.id === sender);
for (const s of sockets) {
if (s.id === sender) continue;
const clientInfo = clientChat.get(s.id);
if (!clientInfo?.user) {
console.log(color.yellow, `Skipping socket ${s.id} (no user found)`);
continue;
}
const user: string = clientChat.get(s.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 !== '') {
s.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}`);
}
});
}
fastify.io.on('connection', (socket: Socket) => {
socket.on('message', (message: string) => {
console.info(
color.blue,
'Socket connected!',
color.reset,
socket.id,
);
console.log(
color.blue,
'Received message from client',
color.reset,
message,
);
// 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,
'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! : ',
});
// 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(obj, obj.SenderWindowID);
console.log(color.red, 'DEBUG LOG: connected in the Chat :', connectedUser(fastify.io), color.reset);
broadcast(fastify, obj, obj.SenderWindowID);
// console.log(color.red, 'DEBUG LOG: connected in the Chat :', connectedUser(fastify.io), color.reset);
});
socket.on('testend', (sock_id_cl: string) => {
console.log('testend received from client socket id:', sock_id_cl);
});
nextGame_SocketListener(fastify, socket);
list_SocketListener(fastify, socket);
socket.on('nextGame', () => {
const link = createNextGame();
const game: Promise<string> = sendGameLinkToChatService(link);
broadcastNextGame(game);
});
// socket.on('list', (object) => {
socket.on('list', (object) => {
// const userFromFrontend = object || null;
// const client = clientChat.get(socket.id) || null;
const userFromFrontend = object || null;
const client = clientChat.get(socket.id) || null;
// //console.log(color.red, 'DEBUG LOG: list activated', userFromFrontend, color.reset, socket.id);
console.log(color.red, 'list activated', userFromFrontend, color.reset, socket.id);
if (userFromFrontend.oldUser !== userFromFrontend.user) {
console.log(color.red, '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);
});
// 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, 'whoAMi activated', userFromFrontend, color.reset, socket.id);
// console.log(color.red, 'DEBUG LOG: whoAMi activated', userFromFrontend, color.reset, socket.id);
if (userFromFrontend.oldUser !== userFromFrontend.user) {
console.log(color.red, 'whoAMi activated', userFromFrontend.oldUser, color.reset);
// 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}'`);
console.log(color.yellow, `'DEBUG LOG: client.user is, '${client.user}'`);
}
}
});
@ -412,7 +194,7 @@ async function onReady(fastify: FastifyInstance) {
timestamp: Date.now(),
SenderWindowID: socket.id,
};
broadcast(obj, socket.id);
broadcast(fastify, obj, socket.id);
// Optional: remove from map
clientChat.delete(socket.id);
// Ensure socket is fully disconnected
@ -440,7 +222,7 @@ async function onReady(fastify: FastifyInstance) {
SenderWindowID: socket.id,
};
broadcast(obj, obj.SenderWindowID);
broadcast(fastify, obj, obj.SenderWindowID);
}
});
@ -464,9 +246,9 @@ async function onReady(fastify: FastifyInstance) {
timestamp: Date.now(),
SenderWindowID: socket.id,
};
console.log(color.blue, 'BROADCASTS OUT :', obj.SenderWindowID);
// console.log(color.blue, 'DEBUG LOG: BROADCASTS OUT :', obj.SenderWindowID);
broadcast(obj, obj.SenderWindowID);
broadcast(fastify, obj, obj.SenderWindowID);
// clientChat.delete(obj.user);
}
});
@ -492,8 +274,8 @@ async function onReady(fastify: FastifyInstance) {
timestamp: Date.now(),
SenderWindowID: socket.id,
};
console.log(color.blue, 'DEBUG LOG: PRIV MESSAGE OUT :', obj.SenderWindowID);
sendPrivMessage(obj, obj.SenderWindowID);
// console.log(color.blue, 'DEBUG LOG: PRIV MESSAGE OUT :', obj.SenderWindowID);
sendPrivMessage(fastify, obj, obj.SenderWindowID);
// clientChat.delete(obj.user);
}
});
@ -502,18 +284,14 @@ async function onReady(fastify: FastifyInstance) {
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 profileHtml: ClientProfil = await getProfil(profilMessage.user, socket);
// 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 :', profileHtml.SenderWindowID);
sendProfil(profileHtml, profileHtml.SenderWindowID);
console.log(color.blue, 'DEBUG - profil message MESSAGE OUT :', profile.SenderWindowID);
sendProfil(fastify, profile, profile.SenderWindowID);
// clientChat.delete(obj.user);
}
});
@ -527,10 +305,87 @@ async function onReady(fastify: FastifyInstance) {
if (clientName !== null) {
// const testuser: User | null = getUserByName(users, profilInvite.user ?? '');
// console.log(color.yellow, 'user:', testuser?.name ?? 'Guest');
sendInvite(inviteHtml, profilInvite);
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)
@ -573,7 +428,7 @@ async function onReady(fastify: FastifyInstance) {
timestamp: Date.now(),
SenderWindowID: socket.id,
};
broadcast(obj, obj.SenderWindowID);
broadcast(fastify, obj, obj.SenderWindowID);
}
});

22
src/chat/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,10 @@
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/chat/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,27 @@
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);
});
}

View file

@ -0,0 +1,53 @@
import { FastifyPluginAsync } from 'fastify';
import { Static, Type } from 'typebox';
import { broadcast } from '../broadcast';
export const ChatReq = Type.Object({
message: Type.String(),
});
export type ChatReq = Static<typeof ChatReq>;
const route: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<{ Body: ChatReq }>(
'/api/chat/broadcast',
{
schema: {
body: ChatReq,
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;

View file

@ -1,53 +0,0 @@
import { FastifyPluginAsync } from 'fastify';
import { MakeStaticResponse, typeResponse } from '@shared/utils';
import { Type } from 'typebox';
// import { UserId } from '@shared/database/mixin/user';
// import { Server } from 'socket.io';
// colors for console.log
export const color = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m',
};
export const ChatRes = {
200: typeResponse('success', 'chat.success', {
name: Type.String(),
id: Type.String(),
guest: Type.Boolean(),
}),
};
export type ChatResType = MakeStaticResponse<typeof ChatRes>;
const route: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get(
'/api/chat/test',
{
schema: {
response: ChatRes,
operationId: 'chatTest',
},
config: { requireAuth: true },
},
async (req, res) => {
// const users = fastify.db.getAllUsers();
// console.log('ALL USERS EVER CONNECTED:', users);
// if (!users) return;
// for (const user of users) {
// console.log(color.yellow, 'USER:', user.name);
// }
// // const usersBlocked = fastify.db.getAllBlockedUsers();
// // console.log(color.red, "ALL BLOCKED USERS:", usersBlocked);
// fastify.db.addBlockedUserFor(users[0].id, users[1].id);
// const usersBlocked2 = fastify.db.getAllBlockedUsers();
// console.log(color.green, 'ALL BLOCKED USERS:', usersBlocked2);
res.makeResponse(200, 'success', 'CCChat.success', { name: 'name', 'id': req.authUser!.id, guest: false });
},
);
};
export default route;

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