@pdc-test/chat-io 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -1,6 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  const WebSocket = require('ws');
3
3
  const readline = require('readline');
4
+ const { exec } = require('child_process');
5
+ const path = require('path');
6
+ const pkgVersion = require('../package.json').version;
7
+
8
+ let soundsEnabled = true;
9
+
10
+ function playSound(type) {
11
+ if (!soundsEnabled) return;
12
+ if (type === 'ding') {
13
+ exec(`powershell -c (New-Object Media.SoundPlayer 'C:\\Windows\\Media\\Windows Notify Email.wav').PlaySync();`);
14
+ } else if (type === 'msg') {
15
+ exec(`powershell -c (New-Object Media.SoundPlayer 'C:\\Windows\\Media\\Windows Proximity Notification.wav').PlaySync();`);
16
+ }
17
+ }
4
18
 
5
19
  // ============================================
6
20
  // CLIENTE MODULAR RÁPIDO (Carga NATIVA JSON)
@@ -33,16 +47,46 @@ function clearTopLine() {
33
47
  }
34
48
 
35
49
  // Interfaz del Teclado Local
36
- const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/all', '/blink'];
50
+ const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/all', '/blink', '/exit', '/clear', '/sound', '/v', '/version'];
51
+ let knownUsersList = [];
52
+
37
53
  function completer(line) {
38
- const hits = commands.filter((c) => c.startsWith(line));
54
+ const tokens = line.split(" ");
55
+ let completions = [];
56
+
57
+ if (line.startsWith("@")) {
58
+ completions = knownUsersList.map(u => "@" + u);
59
+ } else if (line.toLowerCase().startsWith("/w ") || line.toLowerCase().startsWith("/chat ")) {
60
+ completions = knownUsersList.map(u => tokens[0] + " " + u + " ");
61
+ } else if (line.toLowerCase().startsWith("/rps ")) {
62
+ if (tokens.length === 2) {
63
+ completions = knownUsersList.map(u => "/rps " + u + " ");
64
+ } else if (tokens.length === 3) {
65
+ const rpsOpts = ["piedra", "papel", "tijera"];
66
+ completions = rpsOpts.map(o => `/rps ${tokens[1]} ${o}`);
67
+ }
68
+ } else if (line.includes("@")) {
69
+ const lastAt = line.lastIndexOf("@");
70
+ const prefix = line.substring(0, lastAt);
71
+ completions = knownUsersList.map(u => prefix + "@" + u + " ");
72
+ } else {
73
+ completions = [...commands, ...knownUsersList.map(u => "/w " + u)];
74
+ }
75
+
76
+ const hits = completions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase()));
39
77
  return [hits.length ? hits : [], line];
40
78
  }
41
79
 
42
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer, prompt: '> ' });
80
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer, prompt: '> ', removeHistoryDuplicates: true });
81
+
82
+ rl.on('SIGINT', () => {
83
+ printMessage("\x1b[33mSaliendo...\x1b[0m");
84
+ ws.close();
85
+ process.exit(0);
86
+ });
43
87
 
44
88
  process.stdin.on('keypress', (str, key) => {
45
- if (key && key.name !== 'return' && key.name !== 'enter' && myName) {
89
+ if (key && key.name !== 'return' && key.name !== 'enter' && key.name !== 'c' && myName) {
46
90
  if (!currentIsTyping && ws.readyState === WebSocket.OPEN) {
47
91
  currentIsTyping = true;
48
92
  ws.send(JSON.stringify({ type: "typing" }));
@@ -113,8 +157,12 @@ ws.on('message', (data) => {
113
157
  activeTypers = res.users.filter(u => u !== myName);
114
158
  break;
115
159
 
160
+ case "users_update":
161
+ if (res.plain) knownUsersList = res.plain.filter(u => u !== myName);
162
+ break;
163
+
116
164
  case "ding":
117
- process.stdout.write("\x07"); // ASCII Bell físico disparado remotamente
165
+ playSound('ding');
118
166
  break;
119
167
 
120
168
  case "users_list":
@@ -127,8 +175,10 @@ ws.on('message', (data) => {
127
175
  if (res.isWhisper) {
128
176
  const dir = res.from === myName ? `Private → ${res.to}` : `${res.emoji} Secreto de ${res.from}`;
129
177
  printMessage(`\n[${getTime()}] 🔒 [${dir}]: \x1b[35m${safeMsg}\x1b[0m`);
178
+ if (res.from !== myName) playSound('ding');
130
179
  } else {
131
180
  printMessage(`\n[${getTime()}] 🌍 [${res.emoji} ${res.from}]: ${safeMsg}`);
181
+ if (res.from !== myName) playSound('msg');
132
182
  }
133
183
  break;
134
184
 
@@ -203,7 +253,22 @@ rl.on('line', (input) => {
203
253
  }
204
254
 
205
255
  // Local Helper Menú
206
- if (line === "/help") {
256
+ if (line.toLowerCase() === "/version" || line.toLowerCase() === "/v") {
257
+ printMessage(`\nℹ️ Versión actual de Chat-IO: \x1b[1m\x1b[33m${pkgVersion}\x1b[0m\n`);
258
+ return;
259
+ }
260
+ if (line.toLowerCase().startsWith("/sound")) {
261
+ soundsEnabled = !soundsEnabled;
262
+ printMessage(`\n🔊 Sonidos ahora están: \x1b[1m\x1b[33m${soundsEnabled ? "ACTIVADOS" : "DESACTIVADOS"}\x1b[0m\n`);
263
+ return;
264
+ }
265
+ if (line.toLowerCase() === "/clear") { console.clear(); rl.setPrompt('> '); rl.prompt(true); return; }
266
+ if (line.toLowerCase() === "/exit") {
267
+ printMessage("\x1b[33mSaliendo...\x1b[0m");
268
+ ws.close();
269
+ process.exit(0);
270
+ }
271
+ if (line.toLowerCase() === "/help") {
207
272
  printMessage(`\n──────────── 💡 COMANDOS NATIVOS ────────────
208
273
  👥 /users → Ver conectados remotamente
209
274
  🪙 /flip → Lanzar moneda 60FPS
@@ -212,6 +277,10 @@ rl.on('line', (input) => {
212
277
  🔒 /w <usr> [msj] → Modo Susurro directo a una persona
213
278
  🌍 /all → Volver a la Sala Global Abierta
214
279
  🏪 /tiendita → Letrero de Neón de colores acelerado
280
+ 🧹 /clear → Limpia historial de consola
281
+ 🔊 /sound → Activa y desactiva notificaciones y sonidos
282
+ 🏷️ /version → Version del producto
283
+ ❌ /exit → Salir del chat
215
284
  ℹ️ /help → Te ayuda desde la memoria Caché
216
285
  ─────────────────────────────────────────────\n`);
217
286
  return;
@@ -219,18 +288,27 @@ rl.on('line', (input) => {
219
288
 
220
289
  // Constructor Arquitectónico y Limpio de Comandos a Servidor
221
290
  if (line.startsWith("/")) {
222
- if (line === "/users" || line === "/flip" || line === "/tiendita") {
223
- ws.send(JSON.stringify({ type: "command", cmd: line.substring(1) }));
224
- } else if (line === "/all") {
291
+ const lowerLine = line.toLowerCase();
292
+ if (lowerLine === "/users" || lowerLine === "/flip" || lowerLine === "/tiendita") {
293
+ ws.send(JSON.stringify({ type: "command", cmd: lowerLine.substring(1) }));
294
+ } else if (lowerLine === "/all") {
225
295
  ws.send(JSON.stringify({ type: "command", cmd: "mode_all" }));
226
- } else if (line.startsWith("/w ")) {
296
+ } else if (lowerLine.startsWith("/w ")) {
227
297
  const p = line.split(" ");
228
- if (p.length === 2) ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
229
- else if (p.length > 2) ws.send(JSON.stringify({ type: "chat", target: p[1], msg: p.slice(2).join(" ") }));
230
- } else if (line.startsWith("/rps ")) {
298
+ if (p.length === 2) {
299
+ ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
300
+ } else if (p.length > 2) {
301
+ const inlineMsg = p.slice(2).join(" ").trim();
302
+ if (inlineMsg) {
303
+ ws.send(JSON.stringify({ type: "chat", target: p[1], msg: inlineMsg }));
304
+ } else {
305
+ ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
306
+ }
307
+ }
308
+ } else if (lowerLine.startsWith("/rps ")) {
231
309
  const p = line.split(" ");
232
- if (p.length >= 3) ws.send(JSON.stringify({ type: "command", cmd: "rps", target: p[1], choice: p[2] }));
233
- } else if (line.startsWith("/blink ")) {
310
+ if (p.length >= 3) ws.send(JSON.stringify({ type: "command", cmd: "rps", target: p[1], choice: p[2].toLowerCase() }));
311
+ } else if (lowerLine.startsWith("/blink ")) {
234
312
  ws.send(JSON.stringify({ type: "command", cmd: "blink", msg: line.substring(7) }));
235
313
  } else {
236
314
  printMessage(`❌ Comando desconocido. Intenta /help`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdc-test/chat-io",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "main": "./src/index.js",
5
5
  "bin": {
6
6
  "chat-io": "bin/cli.js"
@@ -9,7 +9,7 @@
9
9
  "start": "node src/server/index.js"
10
10
  },
11
11
  "dependencies": {
12
- "@pdc-test/chat-io": "^1.1.0",
12
+ "@pdc-test/chat-io": "^1.1.2",
13
13
  "crypto-js": "^4.2.0",
14
14
  "ws": "^8.0.0"
15
15
  }
package/src/chat/index.js CHANGED
@@ -24,19 +24,39 @@ function startChat(onExit, mode = 'udp') {
24
24
  let animationInterval;
25
25
 
26
26
  function completer(line) {
27
- const completions = [
28
- ...Array.from(knownUsers).map(u => `/chat ${u}`),
29
- ...Array.from(knownUsers).map(u => `/rps ${u} `),
30
- "/all", "/clear", "/help", "/users", "/exit", "/flip", "/blink ", "/chat ", "/rps "
31
- ];
32
- const hits = completions.filter((c) => c.startsWith(line));
27
+ const tokens = line.split(" ");
28
+ let completions = [];
29
+ const cmds = ["/all", "/clear", "/help", "/users", "/exit", "/flip", "/blink", "/chat", "/rps", "/w"];
30
+ const users = Array.from(knownUsers);
31
+
32
+ if (line.startsWith("@")) {
33
+ completions = users.map(u => "@" + u);
34
+ } else if (line.toLowerCase().startsWith("/w ") || line.toLowerCase().startsWith("/chat ")) {
35
+ completions = users.map(u => tokens[0] + " " + u + " ");
36
+ } else if (line.toLowerCase().startsWith("/rps ")) {
37
+ if (tokens.length === 2) {
38
+ completions = users.map(u => "/rps " + u + " ");
39
+ } else if (tokens.length === 3) {
40
+ const rpsOpts = ["piedra", "papel", "tijera"];
41
+ completions = rpsOpts.map(o => `/rps ${tokens[1]} ${o}`);
42
+ }
43
+ } else if (line.includes("@")) {
44
+ const lastAt = line.lastIndexOf("@");
45
+ const prefix = line.substring(0, lastAt);
46
+ completions = users.map(u => prefix + "@" + u + " ");
47
+ } else {
48
+ completions = [...cmds, ...users.map(u => "/w " + u)];
49
+ }
50
+
51
+ const hits = completions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase()));
33
52
  return [hits.length ? hits : completions, line];
34
53
  }
35
54
 
36
55
  const rl = readline.createInterface({
37
56
  input: process.stdin,
38
57
  output: process.stdout,
39
- completer: completer
58
+ completer: completer,
59
+ removeHistoryDuplicates: true
40
60
  });
41
61
 
42
62
  animationInterval = setInterval(() => {
@@ -114,7 +134,10 @@ function startChat(onExit, mode = 'udp') {
114
134
  return;
115
135
  }
116
136
 
117
- if (data.type === "rps_challenge" && data.to === username) {
137
+ // Resolving case insensitivity for target
138
+ const isTargetMe = data.to && data.to.toLowerCase() === username.toLowerCase();
139
+
140
+ if (data.type === "rps_challenge" && isTargetMe) {
118
141
  incomingRPS.add(data.from);
119
142
  readline.clearLine(process.stdout, 0);
120
143
  readline.cursorTo(process.stdout, 0);
@@ -124,11 +147,13 @@ function startChat(onExit, mode = 'udp') {
124
147
  return;
125
148
  }
126
149
 
127
- if (data.type === "rps_response" && data.to === username) {
128
- if (pendingRPS.has(data.from)) {
129
- const miJugada = pendingRPS.get(data.from).miJugada;
150
+ if (data.type === "rps_response" && isTargetMe) {
151
+ // Allow case insensitive checking for pendingRPS
152
+ const pendingKey = Array.from(pendingRPS.keys()).find(k => k.toLowerCase() === data.from.toLowerCase());
153
+ if (pendingKey) {
154
+ const miJugada = pendingRPS.get(pendingKey).miJugada;
130
155
  const suJugada = data.choice;
131
- pendingRPS.delete(data.from);
156
+ pendingRPS.delete(pendingKey);
132
157
 
133
158
  let resultStr = "😐 Es un empate.";
134
159
  let win = false;
@@ -172,7 +197,7 @@ function startChat(onExit, mode = 'udp') {
172
197
  }
173
198
 
174
199
  if (!data.text) return;
175
- if (data.type === "message" && data.to && data.to !== username) return;
200
+ if (data.type === "message" && data.to && data.to.toLowerCase() !== username.toLowerCase() && data.from.toLowerCase() !== username.toLowerCase()) return;
176
201
 
177
202
  readline.clearLine(process.stdout, 0);
178
203
  readline.cursorTo(process.stdout, 0);
@@ -257,12 +282,19 @@ function startChat(onExit, mode = 'udp') {
257
282
  }, 200);
258
283
  }
259
284
 
285
+ rl.on('SIGINT', () => {
286
+ cleanupAndExit();
287
+ });
288
+
260
289
  rl.on("line", (line) => {
261
290
  if (!username) return;
262
291
  const input = line.trim();
292
+ if (!input) { updatePrompt(); return; }
293
+
294
+ const inputLower = input.toLowerCase();
263
295
 
264
- if (input === "/exit") { cleanupAndExit(); return; }
265
- if (input === "/users") {
296
+ if (inputLower === "/exit") { cleanupAndExit(); return; }
297
+ if (inputLower === "/users") {
266
298
  console.log("──────────── 👥 USUARIOS CONECTADOS ────────────");
267
299
  const usersArr = Array.from(knownUsers);
268
300
  if (usersArr.length === 0) console.log(" 👻 No hay otros usuarios todavía.");
@@ -271,22 +303,39 @@ function startChat(onExit, mode = 'udp') {
271
303
  updatePrompt();
272
304
  return;
273
305
  }
274
- if (input === "/clear") { console.clear(); updatePrompt(); return; }
275
- if (input === "/help") {
306
+ if (inputLower === "/clear") { console.clear(); updatePrompt(); return; }
307
+ if (inputLower === "/help") {
276
308
  console.log("──────────── 💡 COMANDOS DISPONIBLES ────────────");
277
309
  console.log(" 💬 /chat <usuario> → Iniciar chat privado");
310
+ console.log(" 💬 /w <usuario> → Susurro / chat privado");
278
311
  console.log(" 🌍 /all → Volver al chat público");
279
312
  console.log(" 🪙 /flip → Lanzar una moneda");
280
313
  console.log(" 🎮 /rps <usr> <j> → Retar a Piedra/Papel/Tijera");
281
314
  console.log(" 🌟 /blink <msj> → Enviar msj parpadeante");
315
+ console.log(" 🧹 /clear → Limpiar historial de consola");
282
316
  console.log(" ❌ /exit → Salir al menú");
283
317
  updatePrompt();
284
318
  return;
285
319
  }
286
- if (input === "/all") { currentChat = ""; console.log("🌍 Chat público"); updatePrompt(); return; }
287
- if (input.startsWith("/chat ")) { currentChat = input.replace("/chat ", "").trim(); console.log(`🔒 Chat privado con ${currentChat}`); updatePrompt(); return; }
320
+ if (inputLower === "/all") { currentChat = ""; console.log("🌍 Chat público"); updatePrompt(); return; }
321
+ if (inputLower.startsWith("/chat ") || inputLower.startsWith("/w ")) {
322
+ const parts = input.split(" ");
323
+ currentChat = parts[1] || "";
324
+ if (parts.length > 2) {
325
+ const inlineMsg = parts.slice(2).join(" ").trim();
326
+ if (inlineMsg) {
327
+ broadcastData(encrypt(JSON.stringify({ type: "message", from: username, to: currentChat, text: inlineMsg, blink: false })));
328
+ readline.moveCursor(process.stdout, 0, -1);
329
+ readline.clearLine(process.stdout, 0);
330
+ console.log(`🔒 [Tú ${getUserEmoji(username)} → ${getUserEmoji(currentChat)} ${currentChat}]: ${inlineMsg}`);
331
+ updatePrompt();
332
+ return;
333
+ }
334
+ }
335
+ console.log(`🔒 Chat privado con ${currentChat}`); updatePrompt(); return;
336
+ }
288
337
 
289
- if (input === "/flip") {
338
+ if (inputLower === "/flip") {
290
339
  const choices = ["CARA", "ESCUDO"];
291
340
  const result = choices[Math.floor(Math.random() * choices.length)];
292
341
  console.log(`🪙 Lanzaste la moneda y salió: \x1b[1m\x1b[33m${result}\x1b[0m`);
@@ -295,19 +344,20 @@ function startChat(onExit, mode = 'udp') {
295
344
  return;
296
345
  }
297
346
 
298
- if (input.startsWith("/rps ")) {
347
+ if (inputLower.startsWith("/rps ")) {
299
348
  const parts = input.split(" ");
300
349
  if (parts.length < 3) { console.log("❌ Usa: /rps <usuario> <piedra|papel|tijera>"); updatePrompt(); return; }
301
350
 
302
351
  const targetUser = parts[1];
303
352
  const jugada = parts[2].toLowerCase();
304
353
  if (!["piedra", "papel", "tijera"].includes(jugada)) { console.log("❌ piedra, papel o tijera."); updatePrompt(); return; }
305
- if (targetUser === username) { console.log("❌ No puedes jugar contigo mismo."); updatePrompt(); return; }
354
+ if (targetUser.toLowerCase() === username.toLowerCase()) { console.log("❌ No puedes jugar contigo mismo."); updatePrompt(); return; }
306
355
 
307
- if (incomingRPS.has(targetUser)) {
308
- incomingRPS.delete(targetUser);
356
+ const incomingKey = Array.from(incomingRPS).find(k => k.toLowerCase() === targetUser.toLowerCase());
357
+ if (incomingKey) {
358
+ incomingRPS.delete(incomingKey);
309
359
  console.log(`🎮 Respuesta enviada (${jugada})...`);
310
- broadcastData(encrypt(JSON.stringify({ type: "rps_response", from: username, to: targetUser, choice: jugada })));
360
+ broadcastData(encrypt(JSON.stringify({ type: "rps_response", from: username, to: incomingKey, choice: jugada })));
311
361
  } else {
312
362
  pendingRPS.set(targetUser, { miJugada: jugada, time: Date.now() });
313
363
  console.log(`🎮 Retaste a ${targetUser} con ${jugada}...`);
@@ -317,7 +367,7 @@ function startChat(onExit, mode = 'udp') {
317
367
  return;
318
368
  }
319
369
 
320
- if (input.startsWith("/") && !input.startsWith("/blink ")) {
370
+ if (inputLower.startsWith("/") && !inputLower.startsWith("/blink ")) {
321
371
  console.log("❌ Comando no reconocido.");
322
372
  updatePrompt();
323
373
  return;
@@ -325,9 +375,9 @@ function startChat(onExit, mode = 'udp') {
325
375
 
326
376
  let textToSend = input;
327
377
  let isBlink = false;
328
- if (input.startsWith("/blink ")) {
378
+ if (inputLower.startsWith("/blink ")) {
329
379
  isBlink = true;
330
- textToSend = input.replace("/blink ", "").trim();
380
+ textToSend = input.substring(7).trim();
331
381
  if (!textToSend) { updatePrompt(); return; }
332
382
  } else if (!input) { updatePrompt(); return; }
333
383
 
@@ -19,24 +19,56 @@ function broadcast(jsonObj, ignoreWs = null) {
19
19
  }
20
20
 
21
21
  function sendToTarget(targetName, jsonObj) {
22
+ if (!targetName) return;
22
23
  const payload = JSON.stringify(jsonObj);
23
24
  for (const [s, data] of sessions.entries()) {
24
- if (data.name === targetName && s.readyState === WebSocket.OPEN) {
25
+ if (data.name && data.name.toLowerCase() === targetName.toLowerCase() && s.readyState === WebSocket.OPEN) {
25
26
  try { s.send(payload); } catch(e) {}
26
27
  }
27
28
  }
28
29
  }
29
30
 
30
31
  function targetExists(name) {
31
- for (const d of sessions.values()) if (d.name === name) return true;
32
+ if (!name) return false;
33
+ for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return true;
32
34
  return false;
33
35
  }
34
36
 
37
+ function getExactName(name) {
38
+ if (!name) return null;
39
+ for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return d.name;
40
+ return name;
41
+ }
42
+
35
43
  // State para Tiping
44
+ // typingMap trackea timeouts y a quién se le está escribiendo
36
45
  let typingMap = new Map();
46
+
47
+ function broadcastUsers() {
48
+ const list = [];
49
+ const plainList = [];
50
+ for(const d of sessions.values()) {
51
+ if(d.name) {
52
+ list.push(`${d.emoji} ${d.name}`);
53
+ plainList.push(d.name);
54
+ }
55
+ }
56
+ broadcast({ type: "users_update", users: list, plain: plainList });
57
+ }
58
+
37
59
  function broadcastTyping() {
38
- const list = Array.from(typingMap.keys());
39
- broadcast({ type: "typing_event", users: list });
60
+ for (const [s, data] of sessions.entries()) {
61
+ if (s.readyState === WebSocket.OPEN && data.name) {
62
+ const visibleTypers = [];
63
+ for (const [typerName, typeData] of typingMap.entries()) {
64
+ // Si está escribiendo en público (target == null) o le escribe directo a este usuario
65
+ if (!typeData.target || typeData.target.toLowerCase() === data.name.toLowerCase()) {
66
+ visibleTypers.push(typerName);
67
+ }
68
+ }
69
+ try { s.send(JSON.stringify({ type: "typing_event", users: visibleTypers })); } catch(e) {}
70
+ }
71
+ }
40
72
  }
41
73
 
42
74
  wss.on("connection", (ws) => {
@@ -64,6 +96,7 @@ wss.on("connection", (ws) => {
64
96
 
65
97
  ws.send(JSON.stringify({ type: "registered", name: user.name, emoji: user.emoji }));
66
98
  broadcast({ type: "system", msg: `🟢 [\x1b[32m${emoji} ${user.name}\x1b[0m] se ha unido al servidor.` }, ws);
99
+ broadcastUsers();
67
100
  return;
68
101
  }
69
102
 
@@ -75,7 +108,7 @@ wss.on("connection", (ws) => {
75
108
  // Limpiar typing automático si envía un mensaje real
76
109
  if (req.type === "chat" || req.type === "command") {
77
110
  if (typingMap.has(myName)) {
78
- clearTimeout(typingMap.get(myName));
111
+ clearTimeout(typingMap.get(myName).timer);
79
112
  typingMap.delete(myName);
80
113
  broadcastTyping();
81
114
  }
@@ -83,17 +116,20 @@ wss.on("connection", (ws) => {
83
116
 
84
117
  switch (req.type) {
85
118
  case "typing":
86
- if (typingMap.has(myName)) clearTimeout(typingMap.get(myName));
87
- typingMap.set(myName, setTimeout(() => {
88
- typingMap.delete(myName);
89
- broadcastTyping();
90
- }, 5000));
119
+ if (typingMap.has(myName)) clearTimeout(typingMap.get(myName).timer);
120
+ typingMap.set(myName, {
121
+ target: user.privateTarget,
122
+ timer: setTimeout(() => {
123
+ typingMap.delete(myName);
124
+ broadcastTyping();
125
+ }, 5000)
126
+ });
91
127
  broadcastTyping();
92
128
  break;
93
129
 
94
130
  case "typing_stop":
95
131
  if (typingMap.has(myName)) {
96
- clearTimeout(typingMap.get(myName));
132
+ clearTimeout(typingMap.get(myName).timer);
97
133
  typingMap.delete(myName);
98
134
  broadcastTyping();
99
135
  }
@@ -108,9 +144,10 @@ wss.on("connection", (ws) => {
108
144
  ws.send(JSON.stringify({ type: "error", msg: `${target} no está conectado.` }));
109
145
  return;
110
146
  }
111
- const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: target };
147
+ const exactTarget = getExactName(target);
148
+ const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: exactTarget };
112
149
  ws.send(JSON.stringify(pMsg));
113
- sendToTarget(target, pMsg);
150
+ sendToTarget(exactTarget, pMsg);
114
151
  } else {
115
152
  const globalMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: false };
116
153
  ws.send(JSON.stringify(globalMsg));
@@ -169,8 +206,13 @@ wss.on("connection", (ws) => {
169
206
  }
170
207
  }
171
208
  else if (req.cmd === "mode_whisper") {
172
- user.privateTarget = req.target;
173
- ws.send(JSON.stringify({ type: "system", msg: `🔒 \x1b[35mModo Privado Fijo con ${req.target}.\x1b[0m Usa /all para salir.` }));
209
+ const exactTarget = getExactName(req.target);
210
+ if (!targetExists(exactTarget)) {
211
+ ws.send(JSON.stringify({ type: "error", msg: `${req.target} no está conectado.` }));
212
+ return;
213
+ }
214
+ user.privateTarget = exactTarget;
215
+ ws.send(JSON.stringify({ type: "system", msg: `🔒 \x1b[35mModo Privado Fijo con ${exactTarget}.\x1b[0m Usa /all para salir.` }));
174
216
  }
175
217
  else if (req.cmd === "mode_all") {
176
218
  user.privateTarget = null;
@@ -190,15 +232,18 @@ wss.on("connection", (ws) => {
190
232
  if (u && u.name) {
191
233
  broadcast({ type: "system", msg: `🔴 [\x1b[31m${u.emoji} ${u.name}\x1b[0m] abandonó la sala.` }, ws);
192
234
  if (typingMap.has(u.name)) {
193
- clearTimeout(typingMap.get(u.name));
235
+ clearTimeout(typingMap.get(u.name).timer);
194
236
  typingMap.delete(u.name);
195
237
  broadcastTyping(); // Notificamos de inmediato al mundo para que eliminen su typing UI
196
238
  }
197
239
  }
198
240
  sessions.delete(ws);
199
- pendingRPS.delete(u ? u.name : "");
200
- for(const [t, data] of pendingRPS.entries()) {
201
- if(data.from === u?.name) pendingRPS.delete(t);
241
+ if (u && u.name) {
242
+ broadcastUsers();
243
+ pendingRPS.delete(u.name);
244
+ for(const [t, data] of pendingRPS.entries()) {
245
+ if(data.from === u.name) pendingRPS.delete(t);
246
+ }
202
247
  }
203
248
  });
204
249
  });
@@ -50,10 +50,10 @@ export default {
50
50
  text = text.trim();
51
51
 
52
52
  if (!socketData.name) {
53
- if (text.startsWith("/name ")) {
54
- const desiredName = text.replace("/name ", "").trim();
53
+ if (text.toLowerCase().startsWith("/name ")) {
54
+ const desiredName = text.substring(6).trim();
55
55
  let alreadyExists = false;
56
- for (const d of sessions.values()) if (d.name === desiredName) alreadyExists = true;
56
+ for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === desiredName.toLowerCase()) alreadyExists = true;
57
57
 
58
58
  if (alreadyExists) {
59
59
  server.send("❌ Ese nombre ya está en uso. Usa /name con otro.");
@@ -70,20 +70,21 @@ export default {
70
70
 
71
71
  const myName = socketData.name;
72
72
  const myEmoji = getUserEmoji(myName);
73
+ const textLower = text.toLowerCase();
73
74
 
74
- if (text === "/help") {
75
+ if (textLower === "/help") {
75
76
  server.send(`\n──────────── 💡 COMANDOS CENTRALIZADOS ────────────\n 👥 /users → Ver conectados\n 🪙 /flip → Lanzar moneda al aire\n 🎮 /rps <usr> <j> → Piedra-Papel-Tijera\n 🌟 /blink <msj> → Mensaje parpadeante\n ℹ️ /help → Esta ayuda\n───────────────────────────────────────────────────\n`);
76
77
  return;
77
78
  }
78
79
 
79
- if (text === "/users") {
80
+ if (textLower === "/users") {
80
81
  let userList = [];
81
82
  for (const d of sessions.values()) if (d.name) userList.push(`${getUserEmoji(d.name)} ${d.name}`);
82
83
  server.send("\n👥 \x1b[36mCONECTADOS AHORA:\x1b[0m " + userList.join(", ") + "\n");
83
84
  return;
84
85
  }
85
86
 
86
- if (text === "/flip") {
87
+ if (textLower === "/flip") {
87
88
  const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
88
89
  const msg = `\n🪙 [${myEmoji} ${myName}] ha lanzado la moneda y cayó en: \x1b[1m\x1b[33m${result}\x1b[0m!\n`;
89
90
  server.send(msg); // <--- ECHO LOCAL agregado
@@ -91,24 +92,32 @@ export default {
91
92
  return;
92
93
  }
93
94
 
94
- if (text.startsWith("/blink ")) {
95
- const blnkText = text.replace("/blink ", "").trim();
95
+ if (textLower.startsWith("/blink ")) {
96
+ const blnkText = text.substring(7).trim();
96
97
  const msg = `\n🌟 [${myEmoji} ${myName}]: \x1b[5m${blnkText}\x1b[0m\n`;
97
98
  server.send(msg); // <--- ECHO LOCAL agregado
98
99
  broadcast(msg, server);
99
100
  return;
100
101
  }
101
102
 
102
- if (text.startsWith("/rps ")) {
103
+ if (textLower.startsWith("/rps ")) {
103
104
  const parts = text.split(" ");
104
105
  if (parts.length < 3) { server.send("❌ Error: /rps <usuario> <piedra|papel|tijera>"); return; }
105
106
  const target = parts[1];
106
107
  const choice = parts[2].toLowerCase();
107
108
 
109
+ let exactTargetName = target;
110
+ for (const d of sessions.values()) {
111
+ if (d.name && d.name.toLowerCase() === target.toLowerCase()) {
112
+ exactTargetName = d.name;
113
+ break;
114
+ }
115
+ }
116
+
108
117
  if (!["piedra", "papel", "tijera"].includes(choice)) { server.send("❌ Elige piedra, papel o tijera."); return; }
109
- if (target === myName) { server.send("❌ No contra ti mismo."); return; }
118
+ if (exactTargetName.toLowerCase() === myName.toLowerCase()) { server.send("❌ No contra ti mismo."); return; }
110
119
 
111
- if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
120
+ if (pendingRPS.has(myName) && pendingRPS.get(myName).from.toLowerCase() === exactTargetName.toLowerCase()) {
112
121
  const retoAnterior = pendingRPS.get(myName);
113
122
  pendingRPS.delete(myName);
114
123
 
@@ -118,20 +127,20 @@ export default {
118
127
  let winStr;
119
128
  if (miJugada === suJugada) { winStr = "😐 Empate"; }
120
129
  else if ( (miJugada === "piedra" && suJugada === "tijera") || (miJugada === "papel" && suJugada === "piedra") || (miJugada === "tijera" && suJugada === "papel") ) { winStr = `🎉 \x1b[32m${myName}!\x1b[0m`; }
121
- else { winStr = `🎉 \x1b[32m${target}!\x1b[0m`; }
130
+ else { winStr = `🎉 \x1b[32m${exactTargetName}!\x1b[0m`; }
122
131
 
123
- const outMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${suJugada}) VS \x1b[36m${myName}\x1b[0m (${miJugada})\n🏆 Ganador: ${winStr}\n`;
132
+ const outMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${exactTargetName}\x1b[0m (${suJugada}) VS \x1b[36m${myName}\x1b[0m (${miJugada})\n🏆 Ganador: ${winStr}\n`;
124
133
  server.send(outMsg); // ECHO LOCAL
125
134
  broadcast(outMsg, server);
126
135
  } else {
127
- pendingRPS.set(target, { from: myName, miJugada: choice });
128
- server.send(`\n🎮 \x1b[33mTu reto (${choice}) ha sido enviado a ${target}.\x1b[0m`);
129
- sendToTarget(target, `\n\x1b[5m\x1b[35m🚨 ¡${myName} te reta a RPS!\x1b[0m\n👉 Responde: /rps ${myName} <piedra|papel|tijera>\n`);
136
+ pendingRPS.set(exactTargetName, { from: myName, miJugada: choice });
137
+ server.send(`\n🎮 \x1b[33mTu reto (${choice}) ha sido enviado a ${exactTargetName}.\x1b[0m`);
138
+ sendToTarget(exactTargetName, `\n\x1b[5m\x1b[35m🚨 ¡${myName} te reta a RPS!\x1b[0m\n👉 Responde: /rps ${myName} <piedra|papel|tijera>\n`);
130
139
  }
131
140
  return;
132
141
  }
133
142
 
134
- if (text.startsWith("/")) {
143
+ if (textLower.startsWith("/")) {
135
144
  server.send("❌ Comando no reconocido. Usa /help");
136
145
  return;
137
146
  }