@pdc-test/chat-io 1.0.0 → 1.0.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 +45 -17
- package/package.json +2 -1
- package/src/server/index.js +78 -35
package/bin/cli.js
CHANGED
|
@@ -25,8 +25,10 @@ const rl = readline.createInterface({
|
|
|
25
25
|
let isTyping = false;
|
|
26
26
|
let typingTimeout = null;
|
|
27
27
|
let typingUsers = new Set();
|
|
28
|
+
let userTypingTimers = {};
|
|
28
29
|
let typingFrames = ['.', '..', '...'];
|
|
29
30
|
let tIdx = 0;
|
|
31
|
+
let isMultilinePrompt = false;
|
|
30
32
|
|
|
31
33
|
// Escuchador de teclado constante (Raw Mode Mágico)
|
|
32
34
|
process.stdin.on('keypress', (str, key) => {
|
|
@@ -42,26 +44,31 @@ process.stdin.on('keypress', (str, key) => {
|
|
|
42
44
|
clearTimeout(typingTimeout);
|
|
43
45
|
typingTimeout = setTimeout(() => {
|
|
44
46
|
isTyping = false;
|
|
45
|
-
|
|
47
|
+
if (ws.readyState === WebSocket.OPEN) ws.send("/typ_stop");
|
|
48
|
+
}, 1500);
|
|
46
49
|
}
|
|
47
50
|
});
|
|
48
51
|
|
|
49
|
-
// Renderizado de "Escribiendo..."
|
|
52
|
+
// Renderizado de "Escribiendo..." en la parte SUPERIOR (En el Prompt)
|
|
50
53
|
setInterval(() => {
|
|
51
54
|
if (typingUsers.size > 0) {
|
|
52
|
-
readline.clearLine(process.stdout, 0);
|
|
53
|
-
readline.cursorTo(process.stdout, 0);
|
|
54
|
-
|
|
55
55
|
const users = Array.from(typingUsers).join(", ");
|
|
56
56
|
const suffix = users.includes(",") ? "están escribiendo" : "está escribiendo";
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
process.stdout.write(`\x1b[90m${users} ${suffix} ${typingFrames[tIdx]}\x1b[0m`);
|
|
58
|
+
rl.setPrompt(`\x1b[90m[${users} ${suffix} ${typingFrames[tIdx]}]\x1b[0m\n> `);
|
|
60
59
|
tIdx = (tIdx + 1) % typingFrames.length;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
rl.prompt(true);
|
|
61
|
+
isMultilinePrompt = true;
|
|
62
|
+
} else {
|
|
63
|
+
if (isMultilinePrompt) {
|
|
64
|
+
rl.setPrompt('> ');
|
|
65
|
+
// Borrar el fantasma superior que dejó el prompt multilinea
|
|
66
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
67
|
+
readline.clearLine(process.stdout, 0);
|
|
68
|
+
readline.moveCursor(process.stdout, 0, 1);
|
|
69
|
+
rl.prompt(true);
|
|
70
|
+
isMultilinePrompt = false;
|
|
71
|
+
}
|
|
65
72
|
}
|
|
66
73
|
}, 400);
|
|
67
74
|
|
|
@@ -79,27 +86,48 @@ ws.on('message', (data) => {
|
|
|
79
86
|
if (msg.startsWith("/typ_event ")) {
|
|
80
87
|
const u = msg.replace("/typ_event ", "").trim();
|
|
81
88
|
typingUsers.add(u);
|
|
82
|
-
|
|
89
|
+
clearTimeout(userTypingTimers[u]);
|
|
90
|
+
userTypingTimers[u] = setTimeout(() => typingUsers.delete(u), 3500);
|
|
83
91
|
return;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
|
|
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
|
|
87
102
|
typingUsers.clear();
|
|
88
103
|
|
|
89
|
-
// Purgamos
|
|
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
|
+
}
|
|
90
111
|
readline.clearLine(process.stdout, 0);
|
|
91
112
|
readline.cursorTo(process.stdout, 0);
|
|
92
113
|
|
|
93
|
-
console.log(msg);
|
|
94
|
-
rl.
|
|
114
|
+
console.log(msg);
|
|
115
|
+
rl.setPrompt('> ');
|
|
116
|
+
rl.prompt(true);
|
|
95
117
|
});
|
|
96
118
|
|
|
97
119
|
// Cuando le damos ENTER
|
|
98
120
|
rl.on('line', (input) => {
|
|
121
|
+
// Borramos la línea que acabas de teclear para que no se duplique
|
|
122
|
+
// con el mensaje que el servidor nos devolverá en azul
|
|
123
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
124
|
+
readline.clearLine(process.stdout, 0);
|
|
125
|
+
|
|
99
126
|
const line = input.trim();
|
|
100
127
|
if (line) {
|
|
101
128
|
ws.send(line);
|
|
102
|
-
isTyping = false;
|
|
129
|
+
isTyping = false;
|
|
130
|
+
ws.send("/typ_stop");
|
|
103
131
|
}
|
|
104
132
|
rl.prompt(true);
|
|
105
133
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdc-test/chat-io",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"chat-io": "bin/cli.js"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"start": "node src/server/index.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@pdc-test/chat-io": "^1.0.2",
|
|
12
13
|
"crypto-js": "^4.2.0",
|
|
13
14
|
"ws": "^8.0.0"
|
|
14
15
|
}
|
package/src/server/index.js
CHANGED
|
@@ -62,7 +62,7 @@ wss.on("connection", (server) => {
|
|
|
62
62
|
|
|
63
63
|
// REGISTRO ÚNICO
|
|
64
64
|
if (!socketData.name) {
|
|
65
|
-
if (!text) return;
|
|
65
|
+
if (!text || text === "/typ") return;
|
|
66
66
|
const desiredName = text.replace("/name ", "").trim();
|
|
67
67
|
if (targetExists(desiredName)) {
|
|
68
68
|
server.send("❌ Nombre en uso. Escribe otro distinto:");
|
|
@@ -88,14 +88,14 @@ wss.on("connection", (server) => {
|
|
|
88
88
|
const myEmoji = socketData.emoji;
|
|
89
89
|
|
|
90
90
|
if (text === "/help") {
|
|
91
|
-
server.send(`\n──────────── 💡 COMANDOS
|
|
91
|
+
server.send(`\n──────────── 💡 COMANDOS ────────────
|
|
92
92
|
👥 /users → Ver conectados
|
|
93
|
-
🪙 /flip → Lanzar moneda
|
|
93
|
+
🪙 /flip → Lanzar moneda
|
|
94
94
|
🎮 /rps <usr> <j> → Piedra-Papel-Tijera
|
|
95
95
|
🌟 /blink <msj> → Mensaje parpadeante
|
|
96
96
|
🔒 /w <usr> <msj> → Mensaje privado
|
|
97
|
-
🏪 /tiendita →
|
|
98
|
-
ℹ️
|
|
97
|
+
🏪 /tiendita → Invitación a la Tiendita
|
|
98
|
+
ℹ️ /help → Esta ayuda
|
|
99
99
|
─────────────────────────────────────────────\n`);
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
@@ -107,51 +107,73 @@ wss.on("connection", (server) => {
|
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
if (text === "/typ") {
|
|
110
|
+
// Indicadores de "Escribiendo" Exclusivos para Smart Clients
|
|
111
|
+
if (text === "/typ" || text === "/typ_stop") {
|
|
112
112
|
for (const [s, data] of sessions.entries()) {
|
|
113
113
|
if (s !== server && s.readyState === WebSocket.OPEN && data.smart) {
|
|
114
|
-
try { s.send(
|
|
114
|
+
try { s.send(`${text}_event ${myEmoji} ${myName}`); } catch (e) { }
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
return; //
|
|
117
|
+
return; // Invisible, solo de control
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
//
|
|
120
|
+
// Restablecer chat a global
|
|
121
|
+
if (text === "/all") {
|
|
122
|
+
socketData.privateTarget = null;
|
|
123
|
+
server.send(`\n[Sistema] 🟢 \x1b[32mModo Chat Público Restaurado.\x1b[0m Ahora todos leerán tus mensajes.\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Mensajes Privados Estáticos y Dinámicos
|
|
121
128
|
if (text.startsWith("/w ")) {
|
|
122
129
|
const parts = text.split(" ");
|
|
123
|
-
if (parts.length < 3) { server.send("❌ Error. Uso: /w <usuario> <mensaje secreto>"); return; }
|
|
124
130
|
const targetUser = parts[1];
|
|
125
|
-
const secretMsg = parts.slice(2).join(" ");
|
|
126
131
|
|
|
127
132
|
if (!targetExists(targetUser)) { server.send("❌ Usuario no encontrado."); return; }
|
|
128
133
|
if (targetUser === myName) { server.send("❌ ¿Hablando contigo mismo?"); return; }
|
|
129
134
|
|
|
130
|
-
|
|
131
|
-
|
|
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`);
|
|
132
146
|
return;
|
|
133
147
|
}
|
|
134
148
|
|
|
135
|
-
// Moneda Animada
|
|
149
|
+
// Moneda Animada ASCII Rotativa in-place
|
|
136
150
|
if (text === "/flip") {
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
];
|
|
139
161
|
|
|
140
|
-
let
|
|
162
|
+
let i = 0;
|
|
141
163
|
const timer = setInterval(() => {
|
|
142
|
-
if (
|
|
143
|
-
const
|
|
144
|
-
server.send(
|
|
145
|
-
broadcast(
|
|
146
|
-
|
|
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++;
|
|
147
169
|
} else {
|
|
148
170
|
clearInterval(timer);
|
|
149
171
|
const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
|
|
150
|
-
const renderMsg = `\
|
|
172
|
+
const renderMsg = `\r\x1b[A\x1b[K[${getTime()}] 🎯 ¡CAYÓ LA MONEDA de ${myName}! Es -> \x1b[1m\x1b[33m${result}\x1b[0m\n`;
|
|
151
173
|
server.send(renderMsg);
|
|
152
174
|
broadcast(renderMsg, server);
|
|
153
175
|
}
|
|
154
|
-
},
|
|
176
|
+
}, 200);
|
|
155
177
|
return;
|
|
156
178
|
}
|
|
157
179
|
|
|
@@ -161,12 +183,12 @@ wss.on("connection", (server) => {
|
|
|
161
183
|
server.send(introMsg);
|
|
162
184
|
broadcast(introMsg, server);
|
|
163
185
|
|
|
164
|
-
const asciiArt = `
|
|
165
|
-
|_ _(_)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
const asciiArt = `_____ _ _ _ _ _ _ _
|
|
187
|
+
|_ _(_) ___ _ __ __| (_) |_ __ _| | | |
|
|
188
|
+
| | | |/ _ \ '_ \ / _ | | __/ _ | | | |
|
|
189
|
+
| | | | __/ | | | (_ | | || (_ |_|_|_|
|
|
190
|
+
|_| |_|\___|_| |_|\__,_|_|\__\__,_(_|_|_)
|
|
191
|
+
`;
|
|
170
192
|
|
|
171
193
|
const baseColors = [
|
|
172
194
|
"\x1b[31m", // Rojo
|
|
@@ -238,10 +260,31 @@ wss.on("connection", (server) => {
|
|
|
238
260
|
return;
|
|
239
261
|
}
|
|
240
262
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
263
|
+
// Analizar Menciones con @
|
|
264
|
+
const formattedText = text.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
|
|
265
|
+
|
|
266
|
+
// Notificar con Ring! a los mencionados (si existen)
|
|
267
|
+
const mentions = [...text.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
|
|
268
|
+
for (const m of mentions) {
|
|
269
|
+
if (targetExists(m) && m !== myName && m !== socketData.privateTarget) {
|
|
270
|
+
sendToTarget(m, "\x07"); // ASCII Bell character
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Chat Normal o Modo Susurro Fijo
|
|
275
|
+
if (socketData.privateTarget) {
|
|
276
|
+
const target = socketData.privateTarget;
|
|
277
|
+
if (!targetExists(target)) {
|
|
278
|
+
server.send(`❌ ${target} ya no está conectado. Escribe /all para salir del modo privado.`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
server.send(`\n[${getTime()}] 🔒 [Private → ${target}]: \x1b[35m${formattedText}\x1b[0m`);
|
|
282
|
+
sendToTarget(target, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${formattedText}\x1b[0m`);
|
|
283
|
+
} else {
|
|
284
|
+
const msg = `\n[${getTime()}] 🌍 [${myEmoji} ${myName}]: ${formattedText}`;
|
|
285
|
+
server.send(msg);
|
|
286
|
+
broadcast(msg, server);
|
|
287
|
+
}
|
|
245
288
|
});
|
|
246
289
|
|
|
247
290
|
server.on("close", () => {
|