@pdc-test/chat-io 1.0.2 → 1.1.1
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 +250 -88
- package/package.json +2 -2
- package/src/chat/index.js +78 -28
- package/src/server/index.js +203 -271
- package/src/server/worker.js +26 -17
package/bin/cli.js
CHANGED
|
@@ -1,140 +1,302 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
2
|
const WebSocket = require('ws');
|
|
4
3
|
const readline = require('readline');
|
|
5
4
|
|
|
6
|
-
//
|
|
5
|
+
// ============================================
|
|
6
|
+
// CLIENTE MODULAR RÁPIDO (Carga NATIVA JSON)
|
|
7
|
+
// ============================================
|
|
7
8
|
const SERVER_URL = 'wss://chat-io-production.up.railway.app/';
|
|
8
|
-
|
|
9
9
|
const ws = new WebSocket(SERVER_URL);
|
|
10
|
-
const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/blink'];
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
let myName = null;
|
|
12
|
+
let currentIsTyping = false;
|
|
13
|
+
let typingTimeout = null;
|
|
14
|
+
|
|
15
|
+
// Control de UI Local
|
|
16
|
+
let activeTypers = [];
|
|
17
|
+
let tIdx = 0;
|
|
18
|
+
const typingFrames = ['.', '..', '...'];
|
|
19
|
+
let isMultilinePrompt = false;
|
|
20
|
+
|
|
21
|
+
// Helpers VISUALES
|
|
22
|
+
function getTime() {
|
|
23
|
+
const d = new Date();
|
|
24
|
+
const hs = String(d.getHours()).padStart(2, '0');
|
|
25
|
+
const ms = String(d.getMinutes()).padStart(2, '0');
|
|
26
|
+
return `\x1b[90m${hs}:${ms}\x1b[0m`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clearTopLine() {
|
|
30
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
31
|
+
readline.clearLine(process.stdout, 0);
|
|
32
|
+
readline.moveCursor(process.stdout, 0, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Interfaz del Teclado Local
|
|
36
|
+
const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/all', '/blink', '/exit', '/clear'];
|
|
37
|
+
let knownUsersList = [];
|
|
38
|
+
|
|
13
39
|
function completer(line) {
|
|
14
|
-
const
|
|
40
|
+
const tokens = line.split(" ");
|
|
41
|
+
let completions = [];
|
|
42
|
+
|
|
43
|
+
if (line.startsWith("@")) {
|
|
44
|
+
completions = knownUsersList.map(u => "@" + u);
|
|
45
|
+
} else if (line.toLowerCase().startsWith("/w ") || line.toLowerCase().startsWith("/chat ")) {
|
|
46
|
+
completions = knownUsersList.map(u => tokens[0] + " " + u + " ");
|
|
47
|
+
} else if (line.toLowerCase().startsWith("/rps ")) {
|
|
48
|
+
if (tokens.length === 2) {
|
|
49
|
+
completions = knownUsersList.map(u => "/rps " + u + " ");
|
|
50
|
+
} else if (tokens.length === 3) {
|
|
51
|
+
const rpsOpts = ["piedra", "papel", "tijera"];
|
|
52
|
+
completions = rpsOpts.map(o => `/rps ${tokens[1]} ${o}`);
|
|
53
|
+
}
|
|
54
|
+
} else if (line.includes("@")) {
|
|
55
|
+
const lastAt = line.lastIndexOf("@");
|
|
56
|
+
const prefix = line.substring(0, lastAt);
|
|
57
|
+
completions = knownUsersList.map(u => prefix + "@" + u + " ");
|
|
58
|
+
} else {
|
|
59
|
+
completions = [...commands, ...knownUsersList.map(u => "/w " + u)];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hits = completions.filter((c) => c.toLowerCase().startsWith(line.toLowerCase()));
|
|
15
63
|
return [hits.length ? hits : [], line];
|
|
16
64
|
}
|
|
17
65
|
|
|
18
|
-
const rl = readline.createInterface({
|
|
19
|
-
input: process.stdin,
|
|
20
|
-
output: process.stdout,
|
|
21
|
-
completer,
|
|
22
|
-
prompt: '\r> ' // Prefix visual del input
|
|
23
|
-
});
|
|
66
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, completer, prompt: '> ', removeHistoryDuplicates: true });
|
|
24
67
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let tIdx = 0;
|
|
31
|
-
let isMultilinePrompt = false;
|
|
68
|
+
rl.on('SIGINT', () => {
|
|
69
|
+
printMessage("\x1b[33mSaliendo...\x1b[0m");
|
|
70
|
+
ws.close();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
});
|
|
32
73
|
|
|
33
|
-
// Escuchador de teclado constante (Raw Mode Mágico)
|
|
34
74
|
process.stdin.on('keypress', (str, key) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
40
|
-
ws.send("/typ"); // Ping silencioso
|
|
41
|
-
}
|
|
75
|
+
if (key && key.name !== 'return' && key.name !== 'enter' && key.name !== 'c' && myName) {
|
|
76
|
+
if (!currentIsTyping && ws.readyState === WebSocket.OPEN) {
|
|
77
|
+
currentIsTyping = true;
|
|
78
|
+
ws.send(JSON.stringify({ type: "typing" }));
|
|
42
79
|
}
|
|
43
|
-
// Reiniciar el timeout si el usuario sigue tecleando
|
|
44
80
|
clearTimeout(typingTimeout);
|
|
45
81
|
typingTimeout = setTimeout(() => {
|
|
46
|
-
|
|
47
|
-
if (ws.readyState === WebSocket.OPEN) ws.send("
|
|
82
|
+
currentIsTyping = false;
|
|
83
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "typing_stop" }));
|
|
48
84
|
}, 1500);
|
|
49
85
|
}
|
|
50
86
|
});
|
|
51
87
|
|
|
52
|
-
//
|
|
88
|
+
// Actualizador de UI Typing Background -> Puesto a 300ms, Cero Network Latency porque es local
|
|
53
89
|
setInterval(() => {
|
|
54
|
-
if (
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
rl.setPrompt(`\x1b[90m[${users} ${suffix} ${typingFrames[tIdx]}]\x1b[0m\n> `);
|
|
90
|
+
if (activeTypers.length > 0) {
|
|
91
|
+
const usersInfo = activeTypers.join(", ") + (activeTypers.length > 1 ? " están" : " está") + " escribiendo";
|
|
92
|
+
rl.setPrompt(`\x1b[90m[${usersInfo} ${typingFrames[tIdx]}]\x1b[0m\n> `);
|
|
59
93
|
tIdx = (tIdx + 1) % typingFrames.length;
|
|
60
94
|
rl.prompt(true);
|
|
61
95
|
isMultilinePrompt = true;
|
|
62
96
|
} else {
|
|
63
97
|
if (isMultilinePrompt) {
|
|
64
98
|
rl.setPrompt('> ');
|
|
65
|
-
|
|
66
|
-
readline.moveCursor(process.stdout, 0, -1);
|
|
67
|
-
readline.clearLine(process.stdout, 0);
|
|
68
|
-
readline.moveCursor(process.stdout, 0, 1);
|
|
99
|
+
clearTopLine();
|
|
69
100
|
rl.prompt(true);
|
|
70
101
|
isMultilinePrompt = false;
|
|
71
102
|
}
|
|
72
103
|
}
|
|
73
|
-
},
|
|
104
|
+
}, 300);
|
|
74
105
|
|
|
75
|
-
// Flujo
|
|
106
|
+
// Flujo Principal Socket
|
|
76
107
|
ws.on('open', () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
108
|
+
console.log("\x1b[36m====================================\x1b[0m");
|
|
109
|
+
console.log(" 🚀 BIENVENIDO AL JSON-CHAT-CLI 🚀");
|
|
110
|
+
console.log("\x1b[36m====================================\x1b[0m");
|
|
111
|
+
console.log("🤖 Sistema: Escribe tu nombre y presiona Enter para comenzar:");
|
|
112
|
+
rl.prompt();
|
|
80
113
|
});
|
|
81
114
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Si el servidor nos avisa secretamente que alguien tipeó:
|
|
86
|
-
if (msg.startsWith("/typ_event ")) {
|
|
87
|
-
const u = msg.replace("/typ_event ", "").trim();
|
|
88
|
-
typingUsers.add(u);
|
|
89
|
-
clearTimeout(userTypingTimers[u]);
|
|
90
|
-
userTypingTimers[u] = setTimeout(() => typingUsers.delete(u), 3500);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (msg.startsWith("/typ_stop_event ")) {
|
|
95
|
-
const u = msg.replace("/typ_stop_event ", "").trim();
|
|
96
|
-
typingUsers.delete(u);
|
|
97
|
-
clearTimeout(userTypingTimers[u]);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Si recibimos texto normal, limpiamos todas las señales tácticas
|
|
102
|
-
typingUsers.clear();
|
|
103
|
-
|
|
104
|
-
// Purgamos toda visualización flotante o entrada corrupta antes de dibujar texto nuevo
|
|
105
|
-
if (isMultilinePrompt) {
|
|
106
|
-
readline.moveCursor(process.stdout, 0, -1);
|
|
107
|
-
readline.clearLine(process.stdout, 0);
|
|
108
|
-
readline.moveCursor(process.stdout, 0, 1);
|
|
109
|
-
isMultilinePrompt = false;
|
|
110
|
-
}
|
|
115
|
+
// Encargado de Pintar Todo en la Terminal limpiamente
|
|
116
|
+
function printMessage(msgStr) {
|
|
117
|
+
if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
|
|
111
118
|
readline.clearLine(process.stdout, 0);
|
|
112
119
|
readline.cursorTo(process.stdout, 0);
|
|
113
|
-
|
|
114
|
-
console.log(msg);
|
|
120
|
+
console.log(msgStr);
|
|
115
121
|
rl.setPrompt('> ');
|
|
116
|
-
rl.prompt(true);
|
|
122
|
+
rl.prompt(true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Receptor de la Nube (Solo procesa comandos JSON Puros, nada de basura String)
|
|
126
|
+
ws.on('message', (data) => {
|
|
127
|
+
let res;
|
|
128
|
+
try { res = JSON.parse(data.toString()); } catch(e) { return; }
|
|
129
|
+
|
|
130
|
+
switch (res.type) {
|
|
131
|
+
case "error": printMessage(`❌ \x1b[31m${res.msg}\x1b[0m`); break;
|
|
132
|
+
|
|
133
|
+
case "system": printMessage(`\n[${getTime()}] ${res.msg}`); break;
|
|
134
|
+
|
|
135
|
+
case "registered":
|
|
136
|
+
myName = res.name;
|
|
137
|
+
printMessage(`\n✅ ¡Listo ${res.name}! Estás con el avatar ${res.emoji}. Escribe /help para los comandos nativos.`);
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case "typing_event":
|
|
141
|
+
// El servidor nos entrega el arreglo COMPLETO y VIVO de quién tipea
|
|
142
|
+
// Tu computadora solo limpia e ignora inteligentemente si está tu mismo nombre de pila
|
|
143
|
+
activeTypers = res.users.filter(u => u !== myName);
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case "users_update":
|
|
147
|
+
if (res.plain) knownUsersList = res.plain.filter(u => u !== myName);
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case "ding":
|
|
151
|
+
process.stdout.write("\x07"); // ASCII Bell físico disparado remotamente
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
case "users_list":
|
|
155
|
+
printMessage(`\n👥 \x1b[36mCONECTADOS AHORA (${res.users.length}):\x1b[0m ` + res.users.join(", ") + "\n");
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case "chat":
|
|
159
|
+
// Todo el poder de Parsear Regex se procesa en TU PC, ahorrándole billones de cálculos al Server
|
|
160
|
+
let safeMsg = res.msg.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
|
|
161
|
+
if (res.isWhisper) {
|
|
162
|
+
const dir = res.from === myName ? `Private → ${res.to}` : `${res.emoji} Secreto de ${res.from}`;
|
|
163
|
+
printMessage(`\n[${getTime()}] 🔒 [${dir}]: \x1b[35m${safeMsg}\x1b[0m`);
|
|
164
|
+
} else {
|
|
165
|
+
printMessage(`\n[${getTime()}] 🌍 [${res.emoji} ${res.from}]: ${safeMsg}`);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case "animation":
|
|
170
|
+
if (res.name === "tiendita") renderTiendita(res.user);
|
|
171
|
+
else if (res.name === "flip") renderFlip(res.user, res.emoji, res.result);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
117
174
|
});
|
|
118
175
|
|
|
119
|
-
//
|
|
176
|
+
// ANIMACIONES RENDERIZADAS LOCALMENTE CON CPU CLIENT (LATENCIA CERO, NO SUCIA LA RED)
|
|
177
|
+
function renderTiendita(user) {
|
|
178
|
+
if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
|
|
179
|
+
readline.clearLine(process.stdout, 0);
|
|
180
|
+
readline.cursorTo(process.stdout, 0);
|
|
181
|
+
|
|
182
|
+
console.log(`\n[${getTime()}] 🏪 \x1b[1m${user}\x1b[0m convoca a la tiendita...\n`);
|
|
183
|
+
|
|
184
|
+
const asciiArt = `_____ _ _ _ _ _ _ _ \n |_ _(_) ___ _ __ __| (_) |_ __ _| | | |\n | | | |/ _ \\ '_ \\ / _ | | __/ _ | | | |\n | | | | __/ | | | (_ | | || (_ |_|_|_|\n |_| |_|\\___|_| |_|\\__,_|_|\\__\\__,_(_|_|_)\n `;
|
|
185
|
+
const colors = ["\x1b[31m", "\x1b[33m", "\x1b[32m", "\x1b[36m", "\x1b[1m\x1b[35m"];
|
|
186
|
+
|
|
187
|
+
// Dejar huella Base
|
|
188
|
+
process.stdout.write(`${colors[0]}${asciiArt}\x1b[0m\n`);
|
|
189
|
+
|
|
190
|
+
let frames = 0;
|
|
191
|
+
const anim = setInterval(() => {
|
|
192
|
+
frames++;
|
|
193
|
+
const c = colors[frames % colors.length];
|
|
194
|
+
// Bucle mágico hiper-veloz usando RAM Local
|
|
195
|
+
process.stdout.write(`\r\x1b[6A\x1b[0J${c}${asciiArt}\x1b[0m\n`);
|
|
196
|
+
if (frames > 15) {
|
|
197
|
+
clearInterval(anim);
|
|
198
|
+
rl.prompt(true);
|
|
199
|
+
}
|
|
200
|
+
}, 150); // Mismo efecto, pero procesado x100 más eficiente
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderFlip(user, emoji, result) {
|
|
204
|
+
if (isMultilinePrompt) { clearTopLine(); isMultilinePrompt = false; }
|
|
205
|
+
readline.clearLine(process.stdout, 0);
|
|
206
|
+
readline.cursorTo(process.stdout, 0);
|
|
207
|
+
console.log(`\n[${getTime()}] 🪙 [${emoji} ${user}] lanza una moneda al aire...`);
|
|
208
|
+
|
|
209
|
+
const asciiCoins = ["\x1b[33m ( o ) \x1b[0m", "\x1b[33m ( | ) \x1b[0m", "\x1b[38;5;220m ( 0 ) \x1b[0m", "\x1b[33m ( | ) \x1b[0m"];
|
|
210
|
+
let i = 0;
|
|
211
|
+
|
|
212
|
+
process.stdout.write(" \n");
|
|
213
|
+
const anim = setInterval(() => {
|
|
214
|
+
process.stdout.write(`\r\x1b[1A\x1b[K ${asciiCoins[i % 4]} zumbando...\n`);
|
|
215
|
+
i++;
|
|
216
|
+
if (i > 15) {
|
|
217
|
+
clearInterval(anim);
|
|
218
|
+
process.stdout.write(`\r\x1b[1A\x1b[K[${getTime()}] 🎯 ¡CAYÓ LA MONEDA de ${user}! Es -> \x1b[1m\x1b[33m${result}\x1b[0m\n\n`);
|
|
219
|
+
rl.prompt(true);
|
|
220
|
+
}
|
|
221
|
+
}, 100);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Control General del Botón Enter (Distribuidor Estructurado)
|
|
120
225
|
rl.on('line', (input) => {
|
|
121
|
-
//
|
|
122
|
-
// con el mensaje que el servidor nos devolverá en azul
|
|
226
|
+
// Destruimos el "echo" por defecto para no dejar basura arriba del prompt
|
|
123
227
|
readline.moveCursor(process.stdout, 0, -1);
|
|
124
228
|
readline.clearLine(process.stdout, 0);
|
|
125
229
|
|
|
126
230
|
const line = input.trim();
|
|
127
|
-
if (line) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
231
|
+
if (!line) { rl.prompt(true); return; }
|
|
232
|
+
|
|
233
|
+
// Flujo Inicial Obligatorio
|
|
234
|
+
if (!myName) {
|
|
235
|
+
ws.send(JSON.stringify({ type: "register", name: line }));
|
|
236
|
+
return;
|
|
131
237
|
}
|
|
132
|
-
|
|
238
|
+
|
|
239
|
+
// Local Helper Menú
|
|
240
|
+
if (line.toLowerCase() === "/clear") { console.clear(); rl.setPrompt('> '); rl.prompt(true); return; }
|
|
241
|
+
if (line.toLowerCase() === "/exit") {
|
|
242
|
+
printMessage("\x1b[33mSaliendo...\x1b[0m");
|
|
243
|
+
ws.close();
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
if (line.toLowerCase() === "/help") {
|
|
247
|
+
printMessage(`\n──────────── 💡 COMANDOS NATIVOS ────────────
|
|
248
|
+
👥 /users → Ver conectados remotamente
|
|
249
|
+
🪙 /flip → Lanzar moneda 60FPS
|
|
250
|
+
🎮 /rps <usr> <j> → Piedra-Papel-Tijera
|
|
251
|
+
🌟 /blink <msj> → Envía un texto parpadeante ANSI
|
|
252
|
+
🔒 /w <usr> [msj] → Modo Susurro directo a una persona
|
|
253
|
+
🌍 /all → Volver a la Sala Global Abierta
|
|
254
|
+
🏪 /tiendita → Letrero de Neón de colores acelerado
|
|
255
|
+
🧹 /clear → Limpia historial de consola
|
|
256
|
+
❌ /exit → Salir del chat
|
|
257
|
+
ℹ️ /help → Te ayuda desde la memoria Caché
|
|
258
|
+
─────────────────────────────────────────────\n`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Constructor Arquitectónico y Limpio de Comandos a Servidor
|
|
263
|
+
if (line.startsWith("/")) {
|
|
264
|
+
const lowerLine = line.toLowerCase();
|
|
265
|
+
if (lowerLine === "/users" || lowerLine === "/flip" || lowerLine === "/tiendita") {
|
|
266
|
+
ws.send(JSON.stringify({ type: "command", cmd: lowerLine.substring(1) }));
|
|
267
|
+
} else if (lowerLine === "/all") {
|
|
268
|
+
ws.send(JSON.stringify({ type: "command", cmd: "mode_all" }));
|
|
269
|
+
} else if (lowerLine.startsWith("/w ")) {
|
|
270
|
+
const p = line.split(" ");
|
|
271
|
+
if (p.length === 2) {
|
|
272
|
+
ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
|
|
273
|
+
} else if (p.length > 2) {
|
|
274
|
+
const inlineMsg = p.slice(2).join(" ").trim();
|
|
275
|
+
if (inlineMsg) {
|
|
276
|
+
ws.send(JSON.stringify({ type: "chat", target: p[1], msg: inlineMsg }));
|
|
277
|
+
} else {
|
|
278
|
+
ws.send(JSON.stringify({ type: "command", cmd: "mode_whisper", target: p[1] }));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else if (lowerLine.startsWith("/rps ")) {
|
|
282
|
+
const p = line.split(" ");
|
|
283
|
+
if (p.length >= 3) ws.send(JSON.stringify({ type: "command", cmd: "rps", target: p[1], choice: p[2].toLowerCase() }));
|
|
284
|
+
} else if (lowerLine.startsWith("/blink ")) {
|
|
285
|
+
ws.send(JSON.stringify({ type: "command", cmd: "blink", msg: line.substring(7) }));
|
|
286
|
+
} else {
|
|
287
|
+
printMessage(`❌ Comando desconocido. Intenta /help`);
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
// Mensaje Casual
|
|
291
|
+
ws.send(JSON.stringify({ type: "chat", msg: line }));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
isTyping = false;
|
|
295
|
+
ws.send(JSON.stringify({ type: "typing_stop" }));
|
|
133
296
|
});
|
|
134
297
|
|
|
298
|
+
// Salvavidas de Desconexión Rápida
|
|
135
299
|
ws.on('close', () => {
|
|
136
|
-
|
|
137
|
-
readline.cursorTo(process.stdout, 0);
|
|
138
|
-
console.log("\n\x1b[31m[!] Se ha perdido la conexión con la Nube.\x1b[0m");
|
|
300
|
+
printMessage("\x1b[31m[!] Oh no... Acabas de perder la conexión con la Matriz (Nube principal caída).\x1b[0m");
|
|
139
301
|
process.exit(0);
|
|
140
302
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdc-test/chat-io",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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.
|
|
12
|
+
"@pdc-test/chat-io": "^1.1.1",
|
|
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
|
@@ -1,305 +1,237 @@
|
|
|
1
|
-
const WebSocket = require(
|
|
2
|
-
|
|
1
|
+
const WebSocket = require('ws');
|
|
3
2
|
const PORT = process.env.PORT || 3000;
|
|
4
3
|
const wss = new WebSocket.Server({ port: PORT });
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const sessions = new Map(); // socket -> { name: string, emoji: string }
|
|
9
|
-
const pendingRPS = new Map(); // targetName -> { from: name, miJugada: "piedra" }
|
|
10
|
-
|
|
5
|
+
// --- ESTADO GLOBAL ---
|
|
6
|
+
const sessions = new Map(); // ws -> { name, emoji, privateTarget }
|
|
11
7
|
const userEmojis = ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷", "🐸", "🐵", "🦄", "🐙", "🦋", "🦖", "🐧", "🦉", "👽", "🤖", "👻", "🥑", "🍕"];
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
const d = new Date();
|
|
15
|
-
const hs = String(d.getHours()).padStart(2, '0');
|
|
16
|
-
const ms = String(d.getMinutes()).padStart(2, '0');
|
|
17
|
-
return `\x1b[90m${hs}:${ms}\x1b[0m`;
|
|
18
|
-
}
|
|
9
|
+
let pendingRPS = new Map(); // target -> { from, miJugada }
|
|
19
10
|
|
|
20
|
-
|
|
11
|
+
// Helpers
|
|
12
|
+
function broadcast(jsonObj, ignoreWs = null) {
|
|
13
|
+
const payload = JSON.stringify(jsonObj);
|
|
21
14
|
for (const [s, data] of sessions.entries()) {
|
|
22
15
|
if (s !== ignoreWs && s.readyState === WebSocket.OPEN && data.name) {
|
|
23
|
-
try { s.send(
|
|
16
|
+
try { s.send(payload); } catch(e) {}
|
|
24
17
|
}
|
|
25
18
|
}
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
function sendToTarget(
|
|
21
|
+
function sendToTarget(targetName, jsonObj) {
|
|
22
|
+
if (!targetName) return;
|
|
23
|
+
const payload = JSON.stringify(jsonObj);
|
|
29
24
|
for (const [s, data] of sessions.entries()) {
|
|
30
|
-
if (data.name
|
|
31
|
-
try { s.send(
|
|
25
|
+
if (data.name && data.name.toLowerCase() === targetName.toLowerCase() && s.readyState === WebSocket.OPEN) {
|
|
26
|
+
try { s.send(payload); } catch(e) {}
|
|
32
27
|
}
|
|
33
28
|
}
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
function targetExists(name) {
|
|
37
|
-
|
|
32
|
+
if (!name) return false;
|
|
33
|
+
for (const d of sessions.values()) if (d.name && d.name.toLowerCase() === name.toLowerCase()) return true;
|
|
38
34
|
return false;
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
server.send("🤖 Sistema: Escribe tu nombre y presiona Enter para comenzar:");
|
|
47
|
-
|
|
48
|
-
server.on("message", (message) => {
|
|
49
|
-
if (globalTienditaTimer) {
|
|
50
|
-
clearInterval(globalTienditaTimer);
|
|
51
|
-
globalTienditaTimer = null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const socketData = sessions.get(server);
|
|
55
|
-
let text = message.toString().trim();
|
|
56
|
-
|
|
57
|
-
// Registro de Smart Client Silencioso
|
|
58
|
-
if (text === "/iam_smart") {
|
|
59
|
-
socketData.smart = true;
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// REGISTRO ÚNICO
|
|
64
|
-
if (!socketData.name) {
|
|
65
|
-
if (!text || text === "/typ") return;
|
|
66
|
-
const desiredName = text.replace("/name ", "").trim();
|
|
67
|
-
if (targetExists(desiredName)) {
|
|
68
|
-
server.send("❌ Nombre en uso. Escribe otro distinto:");
|
|
69
|
-
} else {
|
|
70
|
-
// Lógica de Emojis Únicos sin repetir (Evitamos colisiones)
|
|
71
|
-
const takenEmojis = new Set(Array.from(sessions.values()).map(d => d.emoji).filter(Boolean));
|
|
72
|
-
const available = userEmojis.filter(e => !takenEmojis.has(e));
|
|
73
|
-
|
|
74
|
-
// Si por algun motivo la sala es masiva (> 26 usuarios), reciclamos de todos
|
|
75
|
-
const pool = available.length > 0 ? available : userEmojis;
|
|
76
|
-
const assignedEmoji = pool[Math.floor(Math.random() * pool.length)];
|
|
77
|
-
|
|
78
|
-
socketData.name = desiredName;
|
|
79
|
-
socketData.emoji = assignedEmoji;
|
|
80
|
-
|
|
81
|
-
server.send(`✅ ¡Listo ${desiredName}! Estás conectado con el avatar ${assignedEmoji}. Escribe /help para comandos.`);
|
|
82
|
-
broadcast(`\n[${getTime()}] 🟢 [\x1b[32m${assignedEmoji} ${desiredName}\x1b[0m] se ha unido al servidor.\n`, server);
|
|
83
|
-
}
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const myName = socketData.name;
|
|
88
|
-
const myEmoji = socketData.emoji;
|
|
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
|
+
}
|
|
89
42
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
43
|
+
// State para Tiping
|
|
44
|
+
let typingMap = new Map();
|
|
45
|
+
|
|
46
|
+
function broadcastUsers() {
|
|
47
|
+
const list = [];
|
|
48
|
+
const plainList = [];
|
|
49
|
+
for(const d of sessions.values()) {
|
|
50
|
+
if(d.name) {
|
|
51
|
+
list.push(`${d.emoji} ${d.name}`);
|
|
52
|
+
plainList.push(d.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
broadcast({ type: "users_update", users: list, plain: plainList });
|
|
56
|
+
}
|
|
102
57
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
58
|
+
function broadcastTyping() {
|
|
59
|
+
const list = Array.from(typingMap.keys());
|
|
60
|
+
broadcast({ type: "typing_event", users: list });
|
|
61
|
+
}
|
|
109
62
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
63
|
+
wss.on("connection", (ws) => {
|
|
64
|
+
sessions.set(ws, { name: null, emoji: null, privateTarget: null });
|
|
65
|
+
|
|
66
|
+
ws.on("message", (raw) => {
|
|
67
|
+
let req;
|
|
68
|
+
try { req = JSON.parse(raw); } catch(e) { return; } // Ignorar todo lo que no sea JSON moderno
|
|
69
|
+
|
|
70
|
+
const user = sessions.get(ws);
|
|
71
|
+
|
|
72
|
+
// 1. REGISTRO
|
|
73
|
+
if (req.type === "register") {
|
|
74
|
+
const desiredName = req.name.trim();
|
|
75
|
+
if (targetExists(desiredName)) {
|
|
76
|
+
ws.send(JSON.stringify({ type: "error", msg: "Nombre en uso. Elige otro." }));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const taken = new Set(Array.from(sessions.values()).map(d => d.emoji).filter(Boolean));
|
|
80
|
+
const available = userEmojis.filter(e => !taken.has(e));
|
|
81
|
+
const emoji = available.length > 0 ? available[Math.floor(Math.random() * available.length)] : "⭐";
|
|
82
|
+
|
|
83
|
+
user.name = desiredName;
|
|
84
|
+
user.emoji = emoji;
|
|
85
|
+
|
|
86
|
+
ws.send(JSON.stringify({ type: "registered", name: user.name, emoji: user.emoji }));
|
|
87
|
+
broadcast({ type: "system", msg: `🟢 [\x1b[32m${emoji} ${user.name}\x1b[0m] se ha unido al servidor.` }, ws);
|
|
88
|
+
broadcastUsers();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Seguridad: Debe estar registrado para ejecutar comandos
|
|
93
|
+
if (!user.name) return;
|
|
94
|
+
|
|
95
|
+
const myName = user.name;
|
|
96
|
+
|
|
97
|
+
// Limpiar typing automático si envía un mensaje real
|
|
98
|
+
if (req.type === "chat" || req.type === "command") {
|
|
99
|
+
if (typingMap.has(myName)) {
|
|
100
|
+
clearTimeout(typingMap.get(myName));
|
|
101
|
+
typingMap.delete(myName);
|
|
102
|
+
broadcastTyping();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
switch (req.type) {
|
|
107
|
+
case "typing":
|
|
108
|
+
if (typingMap.has(myName)) clearTimeout(typingMap.get(myName));
|
|
109
|
+
typingMap.set(myName, setTimeout(() => {
|
|
110
|
+
typingMap.delete(myName);
|
|
111
|
+
broadcastTyping();
|
|
112
|
+
}, 5000));
|
|
113
|
+
broadcastTyping();
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case "typing_stop":
|
|
117
|
+
if (typingMap.has(myName)) {
|
|
118
|
+
clearTimeout(typingMap.get(myName));
|
|
119
|
+
typingMap.delete(myName);
|
|
120
|
+
broadcastTyping();
|
|
115
121
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!targetExists(targetUser)) { server.send("❌ Usuario no encontrado."); return; }
|
|
133
|
-
if (targetUser === myName) { server.send("❌ ¿Hablando contigo mismo?"); return; }
|
|
134
|
-
|
|
135
|
-
// Si solo envían /w Mario (Se quedan en modo privado)
|
|
136
|
-
if (parts.length === 2) {
|
|
137
|
-
socketData.privateTarget = targetUser;
|
|
138
|
-
server.send(`\n[Sistema] 🔒 \x1b[35mModo Susurro activado con ${targetUser}.\x1b[0m Todo lo que escribas solo lo leerá él. Escribe /all para salir.\n`);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Si lo envían todo de golpe /w Mario hola
|
|
143
|
-
const secretMsg = parts.slice(2).join(" ");
|
|
144
|
-
server.send(`\n[${getTime()}] 🔒 [Private → ${targetUser}]: \x1b[35m${secretMsg}\x1b[0m`);
|
|
145
|
-
sendToTarget(targetUser, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${secretMsg}\x1b[0m`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Moneda Animada ASCII Rotativa in-place
|
|
150
|
-
if (text === "/flip") {
|
|
151
|
-
const prep = `\n[${getTime()}] 🪙 [${myEmoji} ${myName}] lanza una moneda al aire...\n`;
|
|
152
|
-
server.send(prep);
|
|
153
|
-
broadcast(prep, server);
|
|
154
|
-
|
|
155
|
-
const asciiCoins = [
|
|
156
|
-
"\x1b[33m ( o ) \x1b[0m",
|
|
157
|
-
"\x1b[33m ( | ) \x1b[0m",
|
|
158
|
-
"\x1b[38;5;220m ( 0 ) \x1b[0m",
|
|
159
|
-
"\x1b[33m ( | ) \x1b[0m"
|
|
160
|
-
];
|
|
161
|
-
|
|
162
|
-
let i = 0;
|
|
163
|
-
const timer = setInterval(() => {
|
|
164
|
-
if (i < 15) { // 3 segundos a 200ms
|
|
165
|
-
const frame = `\r\x1b[A\x1b[K ${asciiCoins[i % asciiCoins.length]} zumbando...`;
|
|
166
|
-
server.send(frame);
|
|
167
|
-
broadcast(frame, server);
|
|
168
|
-
i++;
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case "chat":
|
|
125
|
+
const isPrivate = req.target || user.privateTarget;
|
|
126
|
+
const target = req.target || user.privateTarget;
|
|
127
|
+
|
|
128
|
+
if (isPrivate) {
|
|
129
|
+
if (!targetExists(target)) {
|
|
130
|
+
ws.send(JSON.stringify({ type: "error", msg: `${target} no está conectado.` }));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const exactTarget = getExactName(target);
|
|
134
|
+
const pMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: true, to: exactTarget };
|
|
135
|
+
ws.send(JSON.stringify(pMsg));
|
|
136
|
+
sendToTarget(exactTarget, pMsg);
|
|
169
137
|
} else {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
138
|
+
const globalMsg = { type: "chat", from: myName, emoji: user.emoji, msg: req.msg, isWhisper: false };
|
|
139
|
+
ws.send(JSON.stringify(globalMsg));
|
|
140
|
+
broadcast(globalMsg, ws);
|
|
141
|
+
|
|
142
|
+
// Menciones (ding!) enviamos comando JSON puro de ping a clientes
|
|
143
|
+
const mentions = [...req.msg.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
|
|
144
|
+
for (const m of mentions) {
|
|
145
|
+
if (targetExists(m) && m !== myName) {
|
|
146
|
+
sendToTarget(m, { type: "ding" });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
175
149
|
}
|
|
176
|
-
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Comando Animado ASCII de Cambios de Colores
|
|
181
|
-
if (text === "/tiendita") {
|
|
182
|
-
const introMsg = `\n[${getTime()}] 🏪 \x1b[1m${myName}\x1b[0m convoca a todos a la tiendita...\n`;
|
|
183
|
-
server.send(introMsg);
|
|
184
|
-
broadcast(introMsg, server);
|
|
185
|
-
|
|
186
|
-
const asciiArt = `_____ _ _ _ _ _ _ _
|
|
187
|
-
|_ _(_) ___ _ __ __| (_) |_ __ _| | | |
|
|
188
|
-
| | | |/ _ \ '_ \ / _ | | __/ _ | | | |
|
|
189
|
-
| | | | __/ | | | (_ | | || (_ |_|_|_|
|
|
190
|
-
|_| |_|\___|_| |_|\__,_|_|\__\__,_(_|_|_)
|
|
191
|
-
`;
|
|
192
|
-
|
|
193
|
-
const baseColors = [
|
|
194
|
-
"\x1b[31m", // Rojo
|
|
195
|
-
"\x1b[33m", // Amarillo
|
|
196
|
-
"\x1b[32m", // Verde
|
|
197
|
-
"\x1b[36m", // Cyan
|
|
198
|
-
"\x1b[1m\x1b[35m" // Magenta Brillante
|
|
199
|
-
];
|
|
200
|
-
|
|
201
|
-
// Primer frame puro sin código de arrastre hacia arriba
|
|
202
|
-
const f0 = `${baseColors[0]}${asciiArt}\x1b[0m`;
|
|
203
|
-
server.send(f0);
|
|
204
|
-
broadcast(f0, server);
|
|
205
|
-
|
|
206
|
-
let i = 1;
|
|
207
|
-
globalTienditaTimer = setInterval(() => {
|
|
208
|
-
const color = baseColors[i % baseColors.length];
|
|
209
|
-
const frame = `\r\x1b[6A\x1b[0J< ${color}${asciiArt}\x1b[0m`;
|
|
210
|
-
server.send(frame);
|
|
211
|
-
broadcast(frame, server);
|
|
212
|
-
i++;
|
|
213
|
-
}, 600); // Bucle Infinito a 600ms
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (text.startsWith("/blink ")) {
|
|
218
|
-
const blnkText = text.replace("/blink ", "").trim();
|
|
219
|
-
const msg = `\n[${getTime()}] 🌟 [${myEmoji} ${myName}]: \x1b[5m${blnkText}\x1b[0m\n`;
|
|
220
|
-
server.send(msg);
|
|
221
|
-
broadcast(msg, server);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (text.startsWith("/rps ")) {
|
|
226
|
-
const parts = text.split(" ");
|
|
227
|
-
if (parts.length < 3) { server.send("❌ Error: /rps <usuario> <piedra|papel|tijera>"); return; }
|
|
228
|
-
const target = parts[1];
|
|
229
|
-
const choice = parts[2].toLowerCase();
|
|
230
|
-
|
|
231
|
-
if (!["piedra", "papel", "tijera"].includes(choice)) { server.send("❌ Elige piedra, papel o tijera."); return; }
|
|
232
|
-
if (target === myName) { server.send("❌ No contra ti mismo."); return; }
|
|
233
|
-
if (!targetExists(target)) { server.send(`❌ El usuario ${target} no existe.`); return; }
|
|
234
|
-
|
|
235
|
-
if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
|
|
236
|
-
const retoAnterior = pendingRPS.get(myName);
|
|
237
|
-
pendingRPS.delete(myName);
|
|
238
|
-
|
|
239
|
-
const miJugada = choice;
|
|
240
|
-
const suJugada = retoAnterior.miJugada;
|
|
241
|
-
|
|
242
|
-
let winStr;
|
|
243
|
-
if (miJugada === suJugada) { winStr = "😐 Empate"; }
|
|
244
|
-
else if ((miJugada === "piedra" && suJugada === "tijera") || (miJugada === "papel" && suJugada === "piedra") || (miJugada === "tijera" && suJugada === "papel")) { winStr = `🎉 \x1b[32m${myName}!\x1b[0m`; }
|
|
245
|
-
else { winStr = `🎉 \x1b[32m${target}!\x1b[0m`; }
|
|
246
|
-
|
|
247
|
-
const outMsg = `\n[${getTime()}] 🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${suJugada}) VS \x1b[36m${myName}\x1b[0m (${miJugada})\n🏆 Ganador: ${winStr}\n`;
|
|
248
|
-
server.send(outMsg);
|
|
249
|
-
broadcast(outMsg, server);
|
|
250
|
-
} else {
|
|
251
|
-
pendingRPS.set(target, { from: myName, miJugada: choice });
|
|
252
|
-
server.send(`\n[${getTime()}] 🎮 \x1b[33mTu elección escondida (${choice}) ha sido fijada. Esperando a ${target}.\x1b[0m`);
|
|
253
|
-
sendToTarget(target, `\n[${getTime()}] \x1b[5m\x1b[35m🚨 ¡${myName} te reta a muerte en RPS!\x1b[0m\n👉 Responde tu golpe con: /rps ${myName} <piedra|papel|tijera>\n`);
|
|
254
|
-
}
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
150
|
+
break;
|
|
257
151
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
152
|
+
case "command":
|
|
153
|
+
if (req.cmd === "users") {
|
|
154
|
+
const list = [];
|
|
155
|
+
for(const d of sessions.values()) if(d.name) list.push(`${d.emoji} ${d.name}`);
|
|
156
|
+
ws.send(JSON.stringify({ type: "users_list", users: list }));
|
|
157
|
+
}
|
|
158
|
+
else if (req.cmd === "flip") {
|
|
159
|
+
const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
|
|
160
|
+
// Delega el procesado nativo intensivo al cliente! Envía la órden e historial en 1 Ms.
|
|
161
|
+
const animData = { type: "animation", name: "flip", result: result, user: myName, emoji: user.emoji };
|
|
162
|
+
ws.send(JSON.stringify(animData));
|
|
163
|
+
broadcast(animData, ws);
|
|
164
|
+
}
|
|
165
|
+
else if (req.cmd === "tiendita") {
|
|
166
|
+
const animData = { type: "animation", name: "tiendita", user: myName, emoji: user.emoji };
|
|
167
|
+
ws.send(JSON.stringify(animData));
|
|
168
|
+
broadcast(animData, ws);
|
|
169
|
+
}
|
|
170
|
+
else if (req.cmd === "rps") {
|
|
171
|
+
const target = req.target;
|
|
172
|
+
const choice = req.choice;
|
|
173
|
+
if (!targetExists(target)) { ws.send(JSON.stringify({ type: "error", msg: "Usuario no existe" })); return; }
|
|
174
|
+
if (target === myName) { ws.send(JSON.stringify({ type: "error", msg: "No contigo mismo" })); return; }
|
|
175
|
+
|
|
176
|
+
if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
|
|
177
|
+
const reto = pendingRPS.get(myName);
|
|
178
|
+
pendingRPS.delete(myName);
|
|
179
|
+
let win = "😐 Empate";
|
|
180
|
+
if (choice !== reto.miJugada) {
|
|
181
|
+
if ((choice === "piedra" && reto.miJugada === "tijera") || (choice === "papel" && reto.miJugada === "piedra") || (choice === "tijera" && reto.miJugada === "papel")) win = `🎉 Ganador: \x1b[32m${myName}\x1b[0m`;
|
|
182
|
+
else win = `🎉 Ganador: \x1b[32m${target}\x1b[0m`;
|
|
183
|
+
}
|
|
184
|
+
const resMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${reto.miJugada}) VS \x1b[36m${myName}\x1b[0m (${choice})\n🏆 ${win}\n`;
|
|
185
|
+
const resPayload = { type: "system", msg: resMsg };
|
|
186
|
+
ws.send(JSON.stringify(resPayload));
|
|
187
|
+
broadcast(resPayload, ws);
|
|
188
|
+
} else {
|
|
189
|
+
pendingRPS.set(target, { from: myName, miJugada: choice });
|
|
190
|
+
ws.send(JSON.stringify({ type: "system", msg: `🎮 \x1b[33mTu elección escondida (${choice}) ha sido fijada. Esperando a ${target}.\x1b[0m` }));
|
|
191
|
+
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>` });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (req.cmd === "mode_whisper") {
|
|
195
|
+
const exactTarget = getExactName(req.target);
|
|
196
|
+
if (!targetExists(exactTarget)) {
|
|
197
|
+
ws.send(JSON.stringify({ type: "error", msg: `${req.target} no está conectado.` }));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
user.privateTarget = exactTarget;
|
|
201
|
+
ws.send(JSON.stringify({ type: "system", msg: `🔒 \x1b[35mModo Privado Fijo con ${exactTarget}.\x1b[0m Usa /all para salir.` }));
|
|
202
|
+
}
|
|
203
|
+
else if (req.cmd === "mode_all") {
|
|
204
|
+
user.privateTarget = null;
|
|
205
|
+
ws.send(JSON.stringify({ type: "system", msg: `🟢 \x1b[32mModo Chat Público Restaurado.\x1b[0m` }));
|
|
206
|
+
}
|
|
207
|
+
else if (req.cmd === "blink") {
|
|
208
|
+
const bMsg = { type: "system", msg: `🌟 [${user.emoji} ${myName}]: \x1b[5m${req.msg}\x1b[0m` };
|
|
209
|
+
ws.send(JSON.stringify(bMsg));
|
|
210
|
+
broadcast(bMsg, ws);
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
297
213
|
}
|
|
298
214
|
});
|
|
299
215
|
|
|
300
|
-
|
|
301
|
-
|
|
216
|
+
ws.on("close", () => {
|
|
217
|
+
const u = sessions.get(ws);
|
|
218
|
+
if (u && u.name) {
|
|
219
|
+
broadcast({ type: "system", msg: `🔴 [\x1b[31m${u.emoji} ${u.name}\x1b[0m] abandonó la sala.` }, ws);
|
|
220
|
+
if (typingMap.has(u.name)) {
|
|
221
|
+
clearTimeout(typingMap.get(u.name));
|
|
222
|
+
typingMap.delete(u.name);
|
|
223
|
+
broadcastTyping(); // Notificamos de inmediato al mundo para que eliminen su typing UI
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
sessions.delete(ws);
|
|
227
|
+
if (u && u.name) {
|
|
228
|
+
broadcastUsers();
|
|
229
|
+
pendingRPS.delete(u.name);
|
|
230
|
+
for(const [t, data] of pendingRPS.entries()) {
|
|
231
|
+
if(data.from === u.name) pendingRPS.delete(t);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
302
234
|
});
|
|
303
235
|
});
|
|
304
236
|
|
|
305
|
-
console.log(`📡 Servidor
|
|
237
|
+
console.log(`📡 Servidor JSON-Core Central corriendo en puerto ${PORT}`);
|
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
|
}
|