@pdc-test/chat-io 1.0.0 → 1.0.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 +34 -14
- package/package.json +2 -1
- package/src/server/index.js +79 -35
package/bin/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ 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;
|
|
30
31
|
|
|
@@ -42,26 +43,26 @@ process.stdin.on('keypress', (str, key) => {
|
|
|
42
43
|
clearTimeout(typingTimeout);
|
|
43
44
|
typingTimeout = setTimeout(() => {
|
|
44
45
|
isTyping = false;
|
|
45
|
-
|
|
46
|
+
if (ws.readyState === WebSocket.OPEN) ws.send("/typ_stop");
|
|
47
|
+
}, 1500);
|
|
46
48
|
}
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
// Renderizado de "Escribiendo..."
|
|
51
|
+
// Renderizado de "Escribiendo..." en la parte SUPERIOR (En el Prompt)
|
|
50
52
|
setInterval(() => {
|
|
51
53
|
if (typingUsers.size > 0) {
|
|
52
|
-
readline.clearLine(process.stdout, 0);
|
|
53
|
-
readline.cursorTo(process.stdout, 0);
|
|
54
|
-
|
|
55
54
|
const users = Array.from(typingUsers).join(", ");
|
|
56
55
|
const suffix = users.includes(",") ? "están escribiendo" : "está escribiendo";
|
|
57
56
|
|
|
58
|
-
//
|
|
59
|
-
|
|
57
|
+
// Cambiar el diseño del prompt para mostrar texto gris ANTES del "> "
|
|
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
|
+
} else {
|
|
62
|
+
// Restaurar prompt normal cuando nadie escribe
|
|
63
|
+
rl.setPrompt('> ');
|
|
64
|
+
// No forzamos rl.prompt() contínuamente aquí para evitar pestañeo,
|
|
65
|
+
// solo esperamos que rl.prompt() se dibuje en el input o mensaje
|
|
65
66
|
}
|
|
66
67
|
}, 400);
|
|
67
68
|
|
|
@@ -79,12 +80,25 @@ ws.on('message', (data) => {
|
|
|
79
80
|
if (msg.startsWith("/typ_event ")) {
|
|
80
81
|
const u = msg.replace("/typ_event ", "").trim();
|
|
81
82
|
typingUsers.add(u);
|
|
82
|
-
|
|
83
|
+
clearTimeout(userTypingTimers[u]);
|
|
84
|
+
userTypingTimers[u] = setTimeout(() => typingUsers.delete(u), 3500);
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
if (msg.startsWith("/typ_stop ")) {
|
|
89
|
+
const u = msg.replace("/typ_stop ", "").trim();
|
|
90
|
+
typingUsers.delete(u);
|
|
91
|
+
clearTimeout(userTypingTimers[u]);
|
|
92
|
+
|
|
93
|
+
if (typingUsers.size === 0) {
|
|
94
|
+
rl.setPrompt('> ');
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Si recibimos texto normal, limpiamos indicadores que ya terminaron
|
|
87
100
|
typingUsers.clear();
|
|
101
|
+
rl.setPrompt('> ');
|
|
88
102
|
|
|
89
103
|
// Purgamos la línea completa en la consola antes de dibujar la obra de Arte/Chat
|
|
90
104
|
readline.clearLine(process.stdout, 0);
|
|
@@ -96,10 +110,16 @@ ws.on('message', (data) => {
|
|
|
96
110
|
|
|
97
111
|
// Cuando le damos ENTER
|
|
98
112
|
rl.on('line', (input) => {
|
|
113
|
+
// Borramos la línea que acabas de teclear para que no se duplique
|
|
114
|
+
// con el mensaje que el servidor nos devolverá en azul
|
|
115
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
116
|
+
readline.clearLine(process.stdout, 0);
|
|
117
|
+
|
|
99
118
|
const line = input.trim();
|
|
100
119
|
if (line) {
|
|
101
120
|
ws.send(line);
|
|
102
|
-
isTyping = false;
|
|
121
|
+
isTyping = false;
|
|
122
|
+
ws.send("/typ_stop");
|
|
103
123
|
}
|
|
104
124
|
rl.prompt(true);
|
|
105
125
|
});
|
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.1",
|
|
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.1",
|
|
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,13 @@ 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
|
+
|_| |_|\___|_| |_|\__,_|_|\__\__,_(_|_|_)
|
|
192
|
+
`;
|
|
170
193
|
|
|
171
194
|
const baseColors = [
|
|
172
195
|
"\x1b[31m", // Rojo
|
|
@@ -238,10 +261,31 @@ wss.on("connection", (server) => {
|
|
|
238
261
|
return;
|
|
239
262
|
}
|
|
240
263
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
264
|
+
// Analizar Menciones con @
|
|
265
|
+
const formattedText = text.replace(/@([a-zA-Z0-9_]+)/g, "\x1b[1m\x1b[33m@$1\x1b[0m");
|
|
266
|
+
|
|
267
|
+
// Notificar con Ring! a los mencionados (si existen)
|
|
268
|
+
const mentions = [...text.matchAll(/@([a-zA-Z0-9_]+)/g)].map(m => m[1]);
|
|
269
|
+
for (const m of mentions) {
|
|
270
|
+
if (targetExists(m) && m !== myName && m !== socketData.privateTarget) {
|
|
271
|
+
sendToTarget(m, "\x07"); // ASCII Bell character
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Chat Normal o Modo Susurro Fijo
|
|
276
|
+
if (socketData.privateTarget) {
|
|
277
|
+
const target = socketData.privateTarget;
|
|
278
|
+
if (!targetExists(target)) {
|
|
279
|
+
server.send(`❌ ${target} ya no está conectado. Escribe /all para salir del modo privado.`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
server.send(`\n[${getTime()}] 🔒 [Private → ${target}]: \x1b[35m${formattedText}\x1b[0m`);
|
|
283
|
+
sendToTarget(target, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${formattedText}\x1b[0m`);
|
|
284
|
+
} else {
|
|
285
|
+
const msg = `\n[${getTime()}] 🌍 [${myEmoji} ${myName}]: ${formattedText}`;
|
|
286
|
+
server.send(msg);
|
|
287
|
+
broadcast(msg, server);
|
|
288
|
+
}
|
|
245
289
|
});
|
|
246
290
|
|
|
247
291
|
server.on("close", () => {
|