@pdc-test/chat-io 1.1.4 → 1.1.5
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 +175 -175
- package/package.json +2 -2
- package/src/server/index.js +181 -181
package/bin/cli.js
CHANGED
|
@@ -35,16 +35,16 @@ let isMultilinePrompt = false;
|
|
|
35
35
|
|
|
36
36
|
// Helpers VISUALES
|
|
37
37
|
function getTime() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const d = new Date();
|
|
39
|
+
const hs = String(d.getHours()).padStart(2, '0');
|
|
40
|
+
const ms = String(d.getMinutes()).padStart(2, '0');
|
|
41
|
+
return `\x1b[90m${hs}:${ms}\x1b[0m`;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function clearTopLine() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
46
|
+
readline.clearLine(process.stdout, 0);
|
|
47
|
+
readline.moveCursor(process.stdout, 0, 1);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Interfaz del Teclado Local
|
|
@@ -52,30 +52,30 @@ const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/all',
|
|
|
52
52
|
let knownUsersList = [];
|
|
53
53
|
|
|
54
54
|
function completer(line) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
55
|
+
const tokens = line.split(" ");
|
|
56
|
+
let completions = [];
|
|
57
|
+
|
|
58
|
+
if (line.startsWith("@")) {
|
|
59
|
+
completions = knownUsersList.map(u => "@" + u);
|
|
60
|
+
} else if (line.toLowerCase().startsWith("/w ") || line.toLowerCase().startsWith("/chat ")) {
|
|
61
|
+
completions = knownUsersList.map(u => tokens[0] + " " + u + " ");
|
|
62
|
+
} else if (line.toLowerCase().startsWith("/rps ")) {
|
|
63
|
+
if (tokens.length === 2) {
|
|
64
|
+
completions = knownUsersList.map(u => "/rps " + u + " ");
|
|
65
|
+
} else if (tokens.length === 3) {
|
|
66
|
+
const rpsOpts = ["piedra", "papel", "tijera"];
|
|
67
|
+
completions = rpsOpts.map(o => `/rps ${tokens[1]} ${o}`);
|
|
68
|
+
}
|
|
69
|
+
} else if (line.includes("@")) {
|
|
70
|
+
const lastAt = line.lastIndexOf("@");
|
|
71
|
+
const prefix = line.substring(0, lastAt);
|
|
72
|
+
completions = knownUsersList.map(u => prefix + "@" + u + " ");
|
|
73
|
+
} else {
|
|
74
|
+
completions = [...commands, ...knownUsersList.map(u => "/w " + u)];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hits = completions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase()));
|
|
78
|
+
return [hits.length ? hits : [], line];
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer, prompt: '> ', removeHistoryDuplicates: true });
|
|
@@ -88,15 +88,15 @@ rl.on('SIGINT', () => {
|
|
|
88
88
|
|
|
89
89
|
process.stdin.on('keypress', (str, key) => {
|
|
90
90
|
if (key && key.name !== 'return' && key.name !== 'enter' && key.name !== 'c' && myName) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
if (!currentIsTyping && ws.readyState === WebSocket.OPEN) {
|
|
92
|
+
currentIsTyping = true;
|
|
93
|
+
ws.send(JSON.stringify({ type: "typing" }));
|
|
94
|
+
}
|
|
95
|
+
clearTimeout(typingTimeout);
|
|
96
|
+
typingTimeout = setTimeout(() => {
|
|
97
|
+
currentIsTyping = false;
|
|
98
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "typing_stop" }));
|
|
99
|
+
}, 1500);
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
102
|
|
|
@@ -120,74 +120,74 @@ setInterval(() => {
|
|
|
120
120
|
|
|
121
121
|
// Flujo Principal Socket
|
|
122
122
|
ws.on('open', () => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
console.log("\x1b[36m====================================\x1b[0m");
|
|
124
|
+
console.log(" 🚀 BIENVENIDO AL CHAT-IO 🚀");
|
|
125
|
+
console.log("\x1b[36m====================================\x1b[0m");
|
|
126
|
+
console.log("🤖 Sistema: Escribe tu nombre y presiona Enter para comenzar:");
|
|
127
|
+
rl.prompt();
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
// Encargado de Pintar Todo en la Terminal limpiamente
|
|
131
131
|
function printMessage(msgStr) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
|
|
133
|
+
readline.clearLine(process.stdout, 0);
|
|
134
|
+
readline.cursorTo(process.stdout, 0);
|
|
135
|
+
console.log(msgStr);
|
|
136
|
+
rl.setPrompt('> ');
|
|
137
|
+
rl.prompt(true);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// Receptor de la Nube (Solo procesa comandos JSON Puros, nada de basura String)
|
|
141
141
|
ws.on('message', (data) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
142
|
+
let res;
|
|
143
|
+
try { res = JSON.parse(data.toString()); } catch (e) { return; }
|
|
144
|
+
|
|
145
|
+
switch (res.type) {
|
|
146
|
+
case "error": printMessage(`❌ \x1b[31m${res.msg}\x1b[0m`); break;
|
|
147
|
+
|
|
148
|
+
case "system": printMessage(`\n[${getTime()}] ${res.msg}`); break;
|
|
149
|
+
|
|
150
|
+
case "registered":
|
|
151
|
+
myName = res.name;
|
|
152
|
+
printMessage(`\n✅ ¡Listo ${res.name}! Estás con el avatar ${res.emoji}. Escribe /help para los comandos nativos.`);
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case "typing_event":
|
|
156
|
+
// El servidor nos entrega el arreglo COMPLETO y VIVO de quién tipea
|
|
157
|
+
// Tu computadora solo limpia e ignora inteligentemente si está tu mismo nombre de pila
|
|
158
|
+
activeTypers = res.users.filter(u => u !== myName);
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case "users_update":
|
|
162
|
+
if (res.plain) knownUsersList = res.plain.filter(u => u !== myName);
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case "ding":
|
|
166
|
+
playSound('ding');
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case "users_list":
|
|
170
|
+
printMessage(`\n👥 \x1b[36mCONECTADOS AHORA (${res.users.length}):\x1b[0m ` + res.users.join(", ") + "\n");
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case "chat":
|
|
174
|
+
// Todo el poder de Parsear Regex se procesa en TU PC, ahorrándole billones de cálculos al Server
|
|
175
|
+
let safeMsg = res.msg.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
|
|
176
|
+
if (res.isWhisper) {
|
|
177
|
+
const dir = res.from === myName ? `Private → ${res.to}` : `${res.emoji} Secreto de ${res.from}`;
|
|
178
|
+
printMessage(`\n[${getTime()}] 🔒 [${dir}]: \x1b[35m${safeMsg}\x1b[0m`);
|
|
179
|
+
if (res.from !== myName) playSound('ding');
|
|
180
|
+
} else {
|
|
181
|
+
printMessage(`\n[${getTime()}] 🌍 [${res.emoji} ${res.from}]: ${safeMsg}`);
|
|
182
|
+
if (res.from !== myName) playSound('msg');
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case "animation":
|
|
187
|
+
if (res.name === "tiendita") renderTiendita(res.user);
|
|
188
|
+
else if (res.name === "flip") renderFlip(res.user, res.emoji, res.result);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
// ANIMACIONES RENDERIZADAS LOCALMENTE CON CPU CLIENT (LATENCIA CERO, NO SUCIA LA RED)
|
|
@@ -195,15 +195,15 @@ function renderTiendita(user) {
|
|
|
195
195
|
if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
|
|
196
196
|
readline.clearLine(process.stdout, 0);
|
|
197
197
|
readline.cursorTo(process.stdout, 0);
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
console.log(`\n[${getTime()}] 🏪 \x1b[1m${user}\x1b[0m convoca a la tiendita...\n`);
|
|
200
|
-
|
|
200
|
+
|
|
201
201
|
const asciiArt = `_____ _ _ _ _ _ _ _ \n |_ _(_) ___ _ __ __| (_) |_ __ _| | | |\n | | | |/ _ \\ '_ \\ / _ | | __/ _ | | | |\n | | | | __/ | | | (_ | | || (_ |_|_|_|\n |_| |_|\\___|_| |_|\\__,_|_|\\__\\__,_(_|_|_)\n `;
|
|
202
202
|
const colors = ["\x1b[31m", "\x1b[33m", "\x1b[32m", "\x1b[36m", "\x1b[1m\x1b[35m"];
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
// Dejar huella Base
|
|
205
205
|
process.stdout.write(`${colors[0]}${asciiArt}\x1b[0m\n`);
|
|
206
|
-
|
|
206
|
+
|
|
207
207
|
let frames = 0;
|
|
208
208
|
const anim = setInterval(() => {
|
|
209
209
|
frames++;
|
|
@@ -222,10 +222,10 @@ function renderFlip(user, emoji, result) {
|
|
|
222
222
|
readline.clearLine(process.stdout, 0);
|
|
223
223
|
readline.cursorTo(process.stdout, 0);
|
|
224
224
|
console.log(`\n[${getTime()}] 🪙 [${emoji} ${user}] lanza una moneda al aire...`);
|
|
225
|
-
|
|
225
|
+
|
|
226
226
|
const asciiCoins = ["\x1b[33m ( o ) \x1b[0m", "\x1b[33m ( | ) \x1b[0m", "\x1b[38;5;220m ( 0 ) \x1b[0m", "\x1b[33m ( | ) \x1b[0m"];
|
|
227
227
|
let i = 0;
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
process.stdout.write(" \n");
|
|
230
230
|
const anim = setInterval(() => {
|
|
231
231
|
process.stdout.write(`\r\x1b[1A\x1b[K ${asciiCoins[i % 4]} zumbando...\n`);
|
|
@@ -235,42 +235,42 @@ function renderFlip(user, emoji, result) {
|
|
|
235
235
|
process.stdout.write(`\r\x1b[1A\x1b[K[${getTime()}] 🎯 ¡CAYÓ LA MONEDA de ${user}! Es -> \x1b[1m\x1b[33m${result}\x1b[0m\n\n`);
|
|
236
236
|
rl.prompt(true);
|
|
237
237
|
}
|
|
238
|
-
}, 100);
|
|
238
|
+
}, 100);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
// Control General del Botón Enter (Distribuidor Estructurado)
|
|
242
242
|
rl.on('line', (input) => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
243
|
+
// Destruimos el "echo" por defecto para no dejar basura arriba del prompt
|
|
244
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
245
|
+
readline.clearLine(process.stdout, 0);
|
|
246
|
+
|
|
247
|
+
const line = input.trim();
|
|
248
|
+
if (!line) { rl.prompt(true); return; }
|
|
249
|
+
|
|
250
|
+
// Flujo Inicial Obligatorio
|
|
251
|
+
if (!myName) {
|
|
252
|
+
ws.send(JSON.stringify({ type: "register", name: line }));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Local Helper Menú
|
|
257
|
+
if (line.toLowerCase() === "/version" || line.toLowerCase() === "/v") {
|
|
258
|
+
printMessage(`\nℹ️ Versión actual de Chat-IO: \x1b[1m\x1b[33m${pkgVersion}\x1b[0m\n`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (line.toLowerCase().startsWith("/sound")) {
|
|
262
|
+
soundsEnabled = !soundsEnabled;
|
|
263
|
+
printMessage(`\n🔊 Sonidos ahora están: \x1b[1m\x1b[33m${soundsEnabled ? "ACTIVADOS" : "DESACTIVADOS"}\x1b[0m\n`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (line.toLowerCase() === "/clear") { console.clear(); rl.setPrompt('> '); rl.prompt(true); return; }
|
|
267
|
+
if (line.toLowerCase() === "/exit") {
|
|
268
|
+
printMessage("\x1b[33mSaliendo...\x1b[0m");
|
|
269
|
+
ws.close();
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
if (line.toLowerCase() === "/help") {
|
|
273
|
+
printMessage(`\n──────────── 💡 COMANDOS NATIVOS ────────────
|
|
274
274
|
👥 /users → Ver conectados remotamente
|
|
275
275
|
🪙 /flip → Lanzar moneda 60FPS
|
|
276
276
|
🎮 /rps <usr> <j> → Piedra-Papel-Tijera
|
|
@@ -284,43 +284,43 @@ rl.on('line', (input) => {
|
|
|
284
284
|
❌ /exit → Salir del chat
|
|
285
285
|
ℹ️ /help → Te ayuda desde la memoria Caché
|
|
286
286
|
─────────────────────────────────────────────\n`);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Constructor Arquitectónico y Limpio de Comandos a Servidor
|
|
291
|
+
if (line.startsWith("/")) {
|
|
292
|
+
const lowerLine = line.toLowerCase();
|
|
293
|
+
if (lowerLine === "/users" || lowerLine === "/flip" || lowerLine === "/tiendita") {
|
|
294
|
+
ws.send(JSON.stringify({ type: "command", cmd: lowerLine.substring(1) }));
|
|
295
|
+
} else if (lowerLine === "/all") {
|
|
296
|
+
ws.send(JSON.stringify({ type: "command", cmd: "mode_all" }));
|
|
297
|
+
} else if (lowerLine.startsWith("/w ")) {
|
|
298
|
+
const p = line.split(" ");
|
|
299
|
+
if (p.length === 2) {
|
|
300
|
+
ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
|
|
301
|
+
} else if (p.length > 2) {
|
|
302
|
+
const inlineMsg = p.slice(2).join(" ").trim();
|
|
303
|
+
if (inlineMsg) {
|
|
304
|
+
ws.send(JSON.stringify({ type: "chat", target: p[1], msg: inlineMsg }));
|
|
305
|
+
} else {
|
|
306
|
+
ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else if (lowerLine.startsWith("/rps ")) {
|
|
310
|
+
const p = line.split(" ");
|
|
311
|
+
if (p.length >= 3) ws.send(JSON.stringify({ type: "command", cmd: "rps", target: p[1], choice: p[2].toLowerCase() }));
|
|
312
|
+
} else if (lowerLine.startsWith("/blink ")) {
|
|
313
|
+
ws.send(JSON.stringify({ type: "command", cmd: "blink", msg: line.substring(7) }));
|
|
314
|
+
} else {
|
|
315
|
+
printMessage(`❌ Comando desconocido. Intenta /help`);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
// Mensaje Casual
|
|
319
|
+
ws.send(JSON.stringify({ type: "chat", msg: line }));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
currentIsTyping = false;
|
|
323
|
+
ws.send(JSON.stringify({ type: "typing_stop" }));
|
|
324
324
|
});
|
|
325
325
|
|
|
326
326
|
// Salvavidas de Desconexión Rápida
|
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.5",
|
|
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.5",
|
|
13
13
|
"crypto-js": "^4.2.0",
|
|
14
14
|
"ws": "^8.0.0"
|
|
15
15
|
}
|
package/src/server/index.js
CHANGED
|
@@ -10,45 +10,45 @@ let pendingRPS = new Map(); // target -> { from, miJugada }
|
|
|
10
10
|
|
|
11
11
|
// Helpers
|
|
12
12
|
function broadcast(jsonObj, ignoreWs = null) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const payload = JSON.stringify(jsonObj);
|
|
14
|
+
for (const [s, data] of sessions.entries()) {
|
|
15
|
+
if (s !== ignoreWs && s.readyState === WebSocket.OPEN && data.name) {
|
|
16
|
+
try { s.send(payload); } catch (e) { }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function sendToTarget(targetName, jsonObj) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
if (!targetName) return;
|
|
23
|
+
const payload = JSON.stringify(jsonObj);
|
|
24
|
+
for (const [s, data] of sessions.entries()) {
|
|
25
|
+
if (data.name && data.name.toLowerCase() === targetName.toLowerCase() && s.readyState === WebSocket.OPEN) {
|
|
26
|
+
try { s.send(payload); } catch (e) { }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function targetExists(name) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if (!name) return false;
|
|
33
|
+
for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return true;
|
|
34
|
+
return false;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function getExactName(name) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
41
|
}
|
|
42
42
|
|
|
43
43
|
// State para Tiping
|
|
44
44
|
// typingMap trackea timeouts y a quién se le está escribiendo
|
|
45
|
-
let typingMap = new Map();
|
|
45
|
+
let typingMap = new Map();
|
|
46
46
|
|
|
47
47
|
function broadcastUsers() {
|
|
48
48
|
const list = [];
|
|
49
49
|
const plainList = [];
|
|
50
|
-
for(const d of sessions.values()) {
|
|
51
|
-
if(d.name) {
|
|
50
|
+
for (const d of sessions.values()) {
|
|
51
|
+
if (d.name) {
|
|
52
52
|
list.push(`${d.emoji} ${d.name}`);
|
|
53
53
|
plainList.push(d.name);
|
|
54
54
|
}
|
|
@@ -66,186 +66,186 @@ function broadcastTyping() {
|
|
|
66
66
|
visibleTypers.push(typerName);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
try { s.send(JSON.stringify({ type: "typing_event", users: visibleTypers })); } catch(e) {}
|
|
69
|
+
try { s.send(JSON.stringify({ type: "typing_event", users: visibleTypers })); } catch (e) { }
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
wss.on("connection", (ws) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const user = sessions.get(ws);
|
|
82
|
-
|
|
83
|
-
// 1. REGISTRO
|
|
84
|
-
if (req.type === "register") {
|
|
85
|
-
const desiredName = req.name.trim();
|
|
86
|
-
if (targetExists(desiredName)) {
|
|
87
|
-
ws.send(JSON.stringify({ type: "error", msg: "Nombre en uso. Elige otro." }));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const taken = new Set(Array.from(sessions.values()).map(d => d.emoji).filter(Boolean));
|
|
91
|
-
const available = userEmojis.filter(e => !taken.has(e));
|
|
92
|
-
const emoji = available.length > 0 ? available[Math.floor(Math.random() * available.length)] : "⭐";
|
|
93
|
-
|
|
94
|
-
user.name = desiredName;
|
|
95
|
-
user.emoji = emoji;
|
|
96
|
-
|
|
97
|
-
ws.send(JSON.stringify({ type: "registered", name: user.name, emoji: user.emoji }));
|
|
98
|
-
broadcast({ type: "system", msg: `🟢 [\x1b[32m${emoji} ${user.name}\x1b[0m] se ha unido al servidor.` }, ws);
|
|
99
|
-
broadcastUsers();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Seguridad: Debe estar registrado para ejecutar comandos
|
|
104
|
-
if (!user.name) return;
|
|
105
|
-
|
|
106
|
-
const myName = user.name;
|
|
107
|
-
|
|
108
|
-
// Limpiar typing automático si envía un mensaje real
|
|
109
|
-
if (req.type === "chat" || req.type === "command") {
|
|
110
|
-
if (typingMap.has(myName)) {
|
|
111
|
-
clearTimeout(typingMap.get(myName).timer);
|
|
112
|
-
typingMap.delete(myName);
|
|
113
|
-
broadcastTyping();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
switch (req.type) {
|
|
118
|
-
case "typing":
|
|
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
|
-
});
|
|
127
|
-
broadcastTyping();
|
|
128
|
-
break;
|
|
75
|
+
sessions.set(ws, { name: null, emoji: null, privateTarget: null });
|
|
76
|
+
|
|
77
|
+
ws.on("message", (raw) => {
|
|
78
|
+
let req;
|
|
79
|
+
try { req = JSON.parse(raw); } catch (e) { return; } // Ignorar todo lo que no sea JSON moderno
|
|
129
80
|
|
|
130
|
-
|
|
81
|
+
const user = sessions.get(ws);
|
|
82
|
+
|
|
83
|
+
// 1. REGISTRO
|
|
84
|
+
if (req.type === "register") {
|
|
85
|
+
const desiredName = req.name.trim();
|
|
86
|
+
if (targetExists(desiredName)) {
|
|
87
|
+
ws.send(JSON.stringify({ type: "error", msg: "Nombre en uso. Elige otro." }));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const taken = new Set(Array.from(sessions.values()).map(d => d.emoji).filter(Boolean));
|
|
91
|
+
const available = userEmojis.filter(e => !taken.has(e));
|
|
92
|
+
const emoji = available.length > 0 ? available[Math.floor(Math.random() * available.length)] : "⭐";
|
|
93
|
+
|
|
94
|
+
user.name = desiredName;
|
|
95
|
+
user.emoji = emoji;
|
|
96
|
+
|
|
97
|
+
ws.send(JSON.stringify({ type: "registered", name: user.name, emoji: user.emoji }));
|
|
98
|
+
broadcast({ type: "system", msg: `🟢 [\x1b[32m${emoji} ${user.name}\x1b[0m] se ha unido al servidor.` }, ws);
|
|
99
|
+
broadcastUsers();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Seguridad: Debe estar registrado para ejecutar comandos
|
|
104
|
+
if (!user.name) return;
|
|
105
|
+
|
|
106
|
+
const myName = user.name;
|
|
107
|
+
|
|
108
|
+
// Limpiar typing automático si envía un mensaje real
|
|
109
|
+
if (req.type === "chat" || req.type === "command") {
|
|
131
110
|
if (typingMap.has(myName)) {
|
|
132
111
|
clearTimeout(typingMap.get(myName).timer);
|
|
133
112
|
typingMap.delete(myName);
|
|
134
113
|
broadcastTyping();
|
|
135
114
|
}
|
|
136
|
-
|
|
115
|
+
}
|
|
137
116
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
117
|
+
switch (req.type) {
|
|
118
|
+
case "typing":
|
|
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
|
+
});
|
|
127
|
+
broadcastTyping();
|
|
128
|
+
break;
|
|
141
129
|
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const exactTarget = getExactName(target);
|
|
148
|
-
const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: exactTarget };
|
|
149
|
-
ws.send(JSON.stringify(pMsg));
|
|
150
|
-
sendToTarget(exactTarget, pMsg);
|
|
151
|
-
} else {
|
|
152
|
-
const globalMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: false };
|
|
153
|
-
ws.send(JSON.stringify(globalMsg));
|
|
154
|
-
broadcast(globalMsg, ws);
|
|
155
|
-
|
|
156
|
-
// Menciones (ding!) enviamos comando JSON puro de ping a clientes
|
|
157
|
-
const mentions = [...req.msg.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
|
|
158
|
-
for (const m of mentions) {
|
|
159
|
-
if (targetExists(m) && m !== myName) {
|
|
160
|
-
sendToTarget(m, { type: "ding" });
|
|
161
|
-
}
|
|
130
|
+
case "typing_stop":
|
|
131
|
+
if (typingMap.has(myName)) {
|
|
132
|
+
clearTimeout(typingMap.get(myName).timer);
|
|
133
|
+
typingMap.delete(myName);
|
|
134
|
+
broadcastTyping();
|
|
162
135
|
}
|
|
163
|
-
|
|
164
|
-
break;
|
|
136
|
+
break;
|
|
165
137
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// Delega el procesado nativo intensivo al cliente! Envía la órden e historial en 1 Ms.
|
|
175
|
-
const animData = { type: "animation", name: "flip", result: result, user: myName, emoji: user.emoji };
|
|
176
|
-
ws.send(JSON.stringify(animData));
|
|
177
|
-
broadcast(animData, ws);
|
|
178
|
-
}
|
|
179
|
-
else if (req.cmd === "tiendita") {
|
|
180
|
-
const animData = { type: "animation", name: "tiendita", user: myName, emoji: user.emoji };
|
|
181
|
-
ws.send(JSON.stringify(animData));
|
|
182
|
-
broadcast(animData, ws);
|
|
183
|
-
}
|
|
184
|
-
else if (req.cmd === "rps") {
|
|
185
|
-
const target = req.target;
|
|
186
|
-
const choice = req.choice;
|
|
187
|
-
if (!targetExists(target)) { ws.send(JSON.stringify({ type: "error", msg: "Usuario no existe" })); return; }
|
|
188
|
-
if (target === myName) { ws.send(JSON.stringify({ type: "error", msg: "No contigo mismo" })); return; }
|
|
189
|
-
|
|
190
|
-
if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
|
|
191
|
-
const reto = pendingRPS.get(myName);
|
|
192
|
-
pendingRPS.delete(myName);
|
|
193
|
-
let win = "😐 Empate";
|
|
194
|
-
if (choice !== reto.miJugada) {
|
|
195
|
-
if ((choice === "piedra" && reto.miJugada === "tijera") || (choice === "papel" && reto.miJugada === "piedra") || (choice === "tijera" && reto.miJugada === "papel")) win = `🎉 Ganador: \x1b[32m${myName}\x1b[0m`;
|
|
196
|
-
else win = `🎉 Ganador: \x1b[32m${target}\x1b[0m`;
|
|
138
|
+
case "chat":
|
|
139
|
+
const isPrivate = req.target || user.privateTarget;
|
|
140
|
+
const target = req.target || user.privateTarget;
|
|
141
|
+
|
|
142
|
+
if (isPrivate) {
|
|
143
|
+
if (!targetExists(target)) {
|
|
144
|
+
ws.send(JSON.stringify({ type: "error", msg: `${target} no está conectado.` }));
|
|
145
|
+
return;
|
|
197
146
|
}
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
ws.send(JSON.stringify(
|
|
201
|
-
|
|
147
|
+
const exactTarget = getExactName(target);
|
|
148
|
+
const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: exactTarget };
|
|
149
|
+
ws.send(JSON.stringify(pMsg));
|
|
150
|
+
sendToTarget(exactTarget, pMsg);
|
|
202
151
|
} else {
|
|
203
|
-
|
|
204
|
-
ws.send(JSON.stringify(
|
|
205
|
-
|
|
152
|
+
const globalMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: false };
|
|
153
|
+
ws.send(JSON.stringify(globalMsg));
|
|
154
|
+
broadcast(globalMsg, ws);
|
|
155
|
+
|
|
156
|
+
// Menciones (ding!) enviamos comando JSON puro de ping a clientes
|
|
157
|
+
const mentions = [...req.msg.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
|
|
158
|
+
for (const m of mentions) {
|
|
159
|
+
if (targetExists(m) && m !== myName) {
|
|
160
|
+
sendToTarget(m, { type: "ding" });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
206
163
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case "command":
|
|
167
|
+
if (req.cmd === "users") {
|
|
168
|
+
const list = [];
|
|
169
|
+
for (const d of sessions.values()) if (d.name) list.push(`${d.emoji} ${d.name}`);
|
|
170
|
+
ws.send(JSON.stringify({ type: "users_list", users: list }));
|
|
213
171
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
172
|
+
else if (req.cmd === "flip") {
|
|
173
|
+
const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
|
|
174
|
+
// Delega el procesado nativo intensivo al cliente! Envía la órden e historial en 1 Ms.
|
|
175
|
+
const animData = { type: "animation", name: "flip", result: result, user: myName, emoji: user.emoji };
|
|
176
|
+
ws.send(JSON.stringify(animData));
|
|
177
|
+
broadcast(animData, ws);
|
|
178
|
+
}
|
|
179
|
+
else if (req.cmd === "tiendita") {
|
|
180
|
+
const animData = { type: "animation", name: "tiendita", user: myName, emoji: user.emoji };
|
|
181
|
+
ws.send(JSON.stringify(animData));
|
|
182
|
+
broadcast(animData, ws);
|
|
183
|
+
}
|
|
184
|
+
else if (req.cmd === "rps") {
|
|
185
|
+
const target = req.target;
|
|
186
|
+
const choice = req.choice;
|
|
187
|
+
if (!targetExists(target)) { ws.send(JSON.stringify({ type: "error", msg: "Usuario no existe" })); return; }
|
|
188
|
+
if (target === myName) { ws.send(JSON.stringify({ type: "error", msg: "No contigo mismo" })); return; }
|
|
189
|
+
|
|
190
|
+
if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
|
|
191
|
+
const reto = pendingRPS.get(myName);
|
|
192
|
+
pendingRPS.delete(myName);
|
|
193
|
+
let win = "😐 Empate";
|
|
194
|
+
if (choice !== reto.miJugada) {
|
|
195
|
+
if ((choice === "piedra" && reto.miJugada === "tijera") || (choice === "papel" && reto.miJugada === "piedra") || (choice === "tijera" && reto.miJugada === "papel")) win = `🎉 Ganador: \x1b[32m${myName}\x1b[0m`;
|
|
196
|
+
else win = `🎉 Ganador: \x1b[32m${target}\x1b[0m`;
|
|
197
|
+
}
|
|
198
|
+
const resMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${reto.miJugada}) VS \x1b[36m${myName}\x1b[0m (${choice})\n🏆 ${win}\n`;
|
|
199
|
+
const resPayload = { type: "system", msg: resMsg };
|
|
200
|
+
ws.send(JSON.stringify(resPayload));
|
|
201
|
+
broadcast(resPayload, ws);
|
|
202
|
+
} else {
|
|
203
|
+
pendingRPS.set(target, { from: myName, miJugada: choice });
|
|
204
|
+
ws.send(JSON.stringify({ type: "system", msg: `🎮 \x1b[33mTu elección escondida (${choice}) ha sido fijada. Esperando a ${target}.\x1b[0m` }));
|
|
205
|
+
sendToTarget(target, { type: "system", msg: `\x1b[5m\x1b[35m🚨 ¡${myName} te reta a muerte en RPS!\x1b[0m\n👉 Responde tu golpe con: /rps ${myName} <piedra|papel|tijera>` });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (req.cmd === "mode_whisper") {
|
|
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.` }));
|
|
216
|
+
}
|
|
217
|
+
else if (req.cmd === "mode_all") {
|
|
218
|
+
user.privateTarget = null;
|
|
219
|
+
ws.send(JSON.stringify({ type: "system", msg: `🟢 \x1b[32mModo Chat Público Restaurado.\x1b[0m` }));
|
|
220
|
+
}
|
|
221
|
+
else if (req.cmd === "blink") {
|
|
222
|
+
const bMsg = { type: "system", msg: `🌟 [${user.emoji} ${myName}]: \x1b[5m${req.msg}\x1b[0m` };
|
|
223
|
+
ws.send(JSON.stringify(bMsg));
|
|
224
|
+
broadcast(bMsg, ws);
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
ws.on("close", () => {
|
|
231
|
+
const u = sessions.get(ws);
|
|
232
|
+
if (u && u.name) {
|
|
233
|
+
broadcast({ type: "system", msg: `🔴 [\x1b[31m${u.emoji} ${u.name}\x1b[0m] abandonó la sala.` }, ws);
|
|
234
|
+
if (typingMap.has(u.name)) {
|
|
235
|
+
clearTimeout(typingMap.get(u.name).timer);
|
|
236
|
+
typingMap.delete(u.name);
|
|
237
|
+
broadcastTyping(); // Notificamos de inmediato al mundo para que eliminen su typing UI
|
|
220
238
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
239
|
+
}
|
|
240
|
+
sessions.delete(ws);
|
|
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);
|
|
225
246
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
ws.on("close", () => {
|
|
231
|
-
const u = sessions.get(ws);
|
|
232
|
-
if (u && u.name) {
|
|
233
|
-
broadcast({ type: "system", msg: `🔴 [\x1b[31m${u.emoji} ${u.name}\x1b[0m] abandonó la sala.` }, ws);
|
|
234
|
-
if (typingMap.has(u.name)) {
|
|
235
|
-
clearTimeout(typingMap.get(u.name).timer);
|
|
236
|
-
typingMap.delete(u.name);
|
|
237
|
-
broadcastTyping(); // Notificamos de inmediato al mundo para que eliminen su typing UI
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
sessions.delete(ws);
|
|
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
|
-
}
|
|
247
|
-
}
|
|
248
|
-
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
console.log(`📡 Servidor JSON-Core Central corriendo en puerto ${PORT}`);
|