@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 +93 -15
- package/package.json +2 -2
- package/src/chat/index.js +78 -28
- package/src/server/index.js +64 -19
- package/src/server/worker.js +26 -17
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
|
|
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
|
-
|
|
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 === "/
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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 (
|
|
296
|
+
} else if (lowerLine.startsWith("/w ")) {
|
|
227
297
|
const p = line.split(" ");
|
|
228
|
-
if (p.length === 2)
|
|
229
|
-
|
|
230
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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" &&
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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 (
|
|
265
|
-
if (
|
|
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 (
|
|
275
|
-
if (
|
|
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 (
|
|
287
|
-
if (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
308
|
-
|
|
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:
|
|
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 (
|
|
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 (
|
|
378
|
+
if (inputLower.startsWith("/blink ")) {
|
|
329
379
|
isBlink = true;
|
|
330
|
-
textToSend = input.
|
|
380
|
+
textToSend = input.substring(7).trim();
|
|
331
381
|
if (!textToSend) { updatePrompt(); return; }
|
|
332
382
|
} else if (!input) { updatePrompt(); return; }
|
|
333
383
|
|
package/src/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
39
|
-
|
|
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,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
});
|
package/src/server/worker.js
CHANGED
|
@@ -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.
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
95
|
-
const blnkText = text.
|
|
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 (
|
|
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 (
|
|
118
|
+
if (exactTargetName.toLowerCase() === myName.toLowerCase()) { server.send("❌ No contra ti mismo."); return; }
|
|
110
119
|
|
|
111
|
-
if (pendingRPS.has(myName) && pendingRPS.get(myName).from ===
|
|
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${
|
|
130
|
+
else { winStr = `🎉 \x1b[32m${exactTargetName}!\x1b[0m`; }
|
|
122
131
|
|
|
123
|
-
const outMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${
|
|
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(
|
|
128
|
-
server.send(`\n🎮 \x1b[33mTu reto (${choice}) ha sido enviado a ${
|
|
129
|
-
sendToTarget(
|
|
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 (
|
|
143
|
+
if (textLower.startsWith("/")) {
|
|
135
144
|
server.send("❌ Comando no reconocido. Usa /help");
|
|
136
145
|
return;
|
|
137
146
|
}
|