@pdc-test/chat-io 1.0.0
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 +112 -0
- package/package.json +15 -0
- package/src/chat/index.js +378 -0
- package/src/games/rps.js +56 -0
- package/src/index.js +48 -0
- package/src/server/index.js +262 -0
- package/src/server/worker.js +164 -0
- package/src/utils/crypto.js +17 -0
- package/src/utils/emojis.js +13 -0
- package/src/utils/network.js +10 -0
- package/wrangler.toml +3 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const WebSocket = require('ws');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
|
|
6
|
+
// URL del Servidor Oficial de Railway
|
|
7
|
+
const SERVER_URL = 'wss://chat-io-production.up.railway.app/';
|
|
8
|
+
|
|
9
|
+
const ws = new WebSocket(SERVER_URL);
|
|
10
|
+
const commands = ['/users', '/flip', '/rps', '/tiendita', '/help', '/w', '/blink'];
|
|
11
|
+
|
|
12
|
+
// Autocompletado del Teclado (TAB)
|
|
13
|
+
function completer(line) {
|
|
14
|
+
const hits = commands.filter((c) => c.startsWith(line));
|
|
15
|
+
return [hits.length ? hits : [], line];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout,
|
|
21
|
+
completer,
|
|
22
|
+
prompt: '\r> ' // Prefix visual del input
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let isTyping = false;
|
|
26
|
+
let typingTimeout = null;
|
|
27
|
+
let typingUsers = new Set();
|
|
28
|
+
let typingFrames = ['.', '..', '...'];
|
|
29
|
+
let tIdx = 0;
|
|
30
|
+
|
|
31
|
+
// Escuchador de teclado constante (Raw Mode Mágico)
|
|
32
|
+
process.stdin.on('keypress', (str, key) => {
|
|
33
|
+
// Detectamos cualquier tecla que no presione Enter
|
|
34
|
+
if (key && key.name !== 'return' && key.name !== 'enter') {
|
|
35
|
+
if (!isTyping) {
|
|
36
|
+
isTyping = true;
|
|
37
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
38
|
+
ws.send("/typ"); // Ping silencioso
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Reiniciar el timeout si el usuario sigue tecleando
|
|
42
|
+
clearTimeout(typingTimeout);
|
|
43
|
+
typingTimeout = setTimeout(() => {
|
|
44
|
+
isTyping = false;
|
|
45
|
+
}, 2000);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Renderizado de "Escribiendo..." Dinámico en la Barra Inferior
|
|
50
|
+
setInterval(() => {
|
|
51
|
+
if (typingUsers.size > 0) {
|
|
52
|
+
readline.clearLine(process.stdout, 0);
|
|
53
|
+
readline.cursorTo(process.stdout, 0);
|
|
54
|
+
|
|
55
|
+
const users = Array.from(typingUsers).join(", ");
|
|
56
|
+
const suffix = users.includes(",") ? "están escribiendo" : "está escribiendo";
|
|
57
|
+
|
|
58
|
+
// Escribe el texto Gris y luego vuelve al inicio en 0 milisegundos para no interrumpir
|
|
59
|
+
process.stdout.write(`\x1b[90m${users} ${suffix} ${typingFrames[tIdx]}\x1b[0m`);
|
|
60
|
+
tIdx = (tIdx + 1) % typingFrames.length;
|
|
61
|
+
|
|
62
|
+
// Restaurar silenciosamente lo que el usuario estaba redactando
|
|
63
|
+
readline.cursorTo(process.stdout, 0);
|
|
64
|
+
process.stdout.write(`> ${rl.line}`);
|
|
65
|
+
}
|
|
66
|
+
}, 400);
|
|
67
|
+
|
|
68
|
+
// Flujo WebSocket
|
|
69
|
+
ws.on('open', () => {
|
|
70
|
+
// Nos identificamos en secreto como un Cliente VIP Inteligente
|
|
71
|
+
ws.send("/iam_smart");
|
|
72
|
+
// No necesitamos imprimir conexión local, el servidor de Railway enviará el cohete.
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
ws.on('message', (data) => {
|
|
76
|
+
const msg = data.toString();
|
|
77
|
+
|
|
78
|
+
// Si el servidor nos avisa secretamente que alguien tipeó:
|
|
79
|
+
if (msg.startsWith("/typ_event ")) {
|
|
80
|
+
const u = msg.replace("/typ_event ", "").trim();
|
|
81
|
+
typingUsers.add(u);
|
|
82
|
+
setTimeout(() => typingUsers.delete(u), 3000); // 3seg de cooldown maximo
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Si recibimos texto normal, limpiamos indicadores para que no estorben
|
|
87
|
+
typingUsers.clear();
|
|
88
|
+
|
|
89
|
+
// Purgamos la línea completa en la consola antes de dibujar la obra de Arte/Chat
|
|
90
|
+
readline.clearLine(process.stdout, 0);
|
|
91
|
+
readline.cursorTo(process.stdout, 0);
|
|
92
|
+
|
|
93
|
+
console.log(msg); // Imprimimos la maravilla de Colores que envió Railway
|
|
94
|
+
rl.prompt(true); // Forzamos al cursor a volver al lugar de escritura limpio
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Cuando le damos ENTER
|
|
98
|
+
rl.on('line', (input) => {
|
|
99
|
+
const line = input.trim();
|
|
100
|
+
if (line) {
|
|
101
|
+
ws.send(line);
|
|
102
|
+
isTyping = false; // Matamos flag al presionar intro
|
|
103
|
+
}
|
|
104
|
+
rl.prompt(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
ws.on('close', () => {
|
|
108
|
+
readline.clearLine(process.stdout, 0);
|
|
109
|
+
readline.cursorTo(process.stdout, 0);
|
|
110
|
+
console.log("\n\x1b[31m[!] Se ha perdido la conexión con la Nube.\x1b[0m");
|
|
111
|
+
process.exit(0);
|
|
112
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pdc-test/chat-io",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./src/index.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"chat-io": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node src/server/index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"crypto-js": "^4.2.0",
|
|
13
|
+
"ws": "^8.0.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
const dgram = require("dgram");
|
|
2
|
+
const WebSocket = require("ws");
|
|
3
|
+
const readline = require("readline");
|
|
4
|
+
const { encrypt, decrypt } = require("../utils/crypto");
|
|
5
|
+
const { getLocalIps } = require("../utils/network");
|
|
6
|
+
const { getUserEmoji } = require("../utils/emojis");
|
|
7
|
+
|
|
8
|
+
function startChat(onExit, mode = 'udp') {
|
|
9
|
+
console.clear();
|
|
10
|
+
const PORT = 41234;
|
|
11
|
+
const BROADCAST_IP = "172.16.41.255";
|
|
12
|
+
const CLOUD_URL = "wss://chat-backend.marioarita502.workers.dev";
|
|
13
|
+
let socket;
|
|
14
|
+
|
|
15
|
+
let username = "";
|
|
16
|
+
let currentChat = "";
|
|
17
|
+
let knownUsers = new Set();
|
|
18
|
+
let typingUsers = new Map();
|
|
19
|
+
let lastSeen = new Map();
|
|
20
|
+
|
|
21
|
+
let pendingRPS = new Map();
|
|
22
|
+
let incomingRPS = new Set();
|
|
23
|
+
let dotCount = 0;
|
|
24
|
+
let animationInterval;
|
|
25
|
+
|
|
26
|
+
function completer(line) {
|
|
27
|
+
const completions = [
|
|
28
|
+
...Array.from(knownUsers).map(u => `/chat ${u}`),
|
|
29
|
+
...Array.from(knownUsers).map(u => `/rps ${u} `),
|
|
30
|
+
"/all", "/clear", "/help", "/users", "/exit", "/flip", "/blink ", "/chat ", "/rps "
|
|
31
|
+
];
|
|
32
|
+
const hits = completions.filter((c) => c.startsWith(line));
|
|
33
|
+
return [hits.length ? hits : completions, line];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rl = readline.createInterface({
|
|
37
|
+
input: process.stdin,
|
|
38
|
+
output: process.stdout,
|
|
39
|
+
completer: completer
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
animationInterval = setInterval(() => {
|
|
43
|
+
if (typingUsers.size > 0 && username) {
|
|
44
|
+
dotCount = (dotCount + 1) % 4;
|
|
45
|
+
updatePrompt();
|
|
46
|
+
}
|
|
47
|
+
}, 500);
|
|
48
|
+
|
|
49
|
+
function updatePrompt() {
|
|
50
|
+
const typers = Array.from(typingUsers.keys());
|
|
51
|
+
let typingMsg = "";
|
|
52
|
+
if (typers.length > 0) {
|
|
53
|
+
const verb = typers.length > 1 ? "están escribiendo" : "escribiendo";
|
|
54
|
+
const typersWithEmoji = typers.map(t => `${getUserEmoji(t)} ${t}`);
|
|
55
|
+
const dots = ".".repeat(dotCount);
|
|
56
|
+
typingMsg = ` (✏️ ${typersWithEmoji.join(", ")} ${verb}${dots})`;
|
|
57
|
+
}
|
|
58
|
+
rl.setPrompt(currentChat ? `[priv:${currentChat}]${typingMsg} > ` : `[all]${typingMsg} > `);
|
|
59
|
+
rl.prompt(true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function initChatUI() {
|
|
63
|
+
rl.question("Nombre: ", (name) => {
|
|
64
|
+
username = name.trim() || "Anon";
|
|
65
|
+
console.log(`👋 Bienvenido ${username}`);
|
|
66
|
+
console.log("💡 Escribe /help para ver los comandos.");
|
|
67
|
+
|
|
68
|
+
const joinPayload = { type: "join", from: username };
|
|
69
|
+
broadcastData(encrypt(JSON.stringify(joinPayload)));
|
|
70
|
+
|
|
71
|
+
updatePrompt();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function processMessage(cipherText) {
|
|
76
|
+
const decrypted = decrypt(cipherText);
|
|
77
|
+
if (!decrypted) return;
|
|
78
|
+
|
|
79
|
+
let data;
|
|
80
|
+
try { data = JSON.parse(decrypted); } catch { return; }
|
|
81
|
+
|
|
82
|
+
if (data.from && data.from !== username) {
|
|
83
|
+
const isNew = !knownUsers.has(data.from);
|
|
84
|
+
knownUsers.add(data.from);
|
|
85
|
+
lastSeen.set(data.from, Date.now());
|
|
86
|
+
|
|
87
|
+
if (isNew && data.type !== "leave") {
|
|
88
|
+
readline.clearLine(process.stdout, 0);
|
|
89
|
+
readline.cursorTo(process.stdout, 0);
|
|
90
|
+
console.log(`🟢 [${getUserEmoji(data.from)} ${data.from}] conectado.`);
|
|
91
|
+
updatePrompt();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (data.type === "heartbeat" || data.type === "present" || data.type === "join") return;
|
|
96
|
+
|
|
97
|
+
if (data.type === "leave") {
|
|
98
|
+
if (knownUsers.has(data.from)) {
|
|
99
|
+
knownUsers.delete(data.from);
|
|
100
|
+
lastSeen.delete(data.from);
|
|
101
|
+
readline.clearLine(process.stdout, 0);
|
|
102
|
+
readline.cursorTo(process.stdout, 0);
|
|
103
|
+
console.log(`🔴 [${getUserEmoji(data.from)} ${data.from}] salió del chat.`);
|
|
104
|
+
updatePrompt();
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (data.type === "flip") {
|
|
110
|
+
readline.clearLine(process.stdout, 0);
|
|
111
|
+
readline.cursorTo(process.stdout, 0);
|
|
112
|
+
console.log(`🪙 [${getUserEmoji(data.from)} ${data.from}] lanzó moneda: \x1b[1m\x1b[33m${data.result}\x1b[0m!`);
|
|
113
|
+
updatePrompt();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (data.type === "rps_challenge" && data.to === username) {
|
|
118
|
+
incomingRPS.add(data.from);
|
|
119
|
+
readline.clearLine(process.stdout, 0);
|
|
120
|
+
readline.cursorTo(process.stdout, 0);
|
|
121
|
+
console.log(`\n\x1b[5m\x1b[35m🎮 ¡${data.from} te ha retado a Piedra, Papel o Tijera!\x1b[0m`);
|
|
122
|
+
console.log(`👉 Acepta con: /rps ${data.from} <piedra|papel|tijera>\n`);
|
|
123
|
+
updatePrompt();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (data.type === "rps_response" && data.to === username) {
|
|
128
|
+
if (pendingRPS.has(data.from)) {
|
|
129
|
+
const miJugada = pendingRPS.get(data.from).miJugada;
|
|
130
|
+
const suJugada = data.choice;
|
|
131
|
+
pendingRPS.delete(data.from);
|
|
132
|
+
|
|
133
|
+
let resultStr = "😐 Es un empate.";
|
|
134
|
+
let win = false;
|
|
135
|
+
if (miJugada === suJugada) { resultStr = "😐 Es un empate."; }
|
|
136
|
+
else if (
|
|
137
|
+
(miJugada === "piedra" && suJugada === "tijera") ||
|
|
138
|
+
(miJugada === "papel" && suJugada === "piedra") ||
|
|
139
|
+
(miJugada === "tijera" && suJugada === "papel")
|
|
140
|
+
) { resultStr = "🎉 ¡Tú ganas!"; win = true; }
|
|
141
|
+
else { resultStr = `💀 ¡Gana ${data.from}!`; }
|
|
142
|
+
|
|
143
|
+
readline.clearLine(process.stdout, 0);
|
|
144
|
+
readline.cursorTo(process.stdout, 0);
|
|
145
|
+
console.log(`\n🎮 RESULTADO RPS CONTRA ${data.from}:`);
|
|
146
|
+
console.log(`Tu jugada: ${miJugada} | Su jugada: ${suJugada}`);
|
|
147
|
+
console.log(`${resultStr}\n`);
|
|
148
|
+
|
|
149
|
+
const theWinner = win ? username : (resultStr === "😐 Es un empate." ? "Nadie" : data.from);
|
|
150
|
+
const broadPld = { type: "message", from: "Sistema", text: `🕹️ RPS Resultado: \x1b[36m${username}\x1b[0m (${miJugada}) vs \x1b[36m${data.from}\x1b[0m (${suJugada}) -> Ganó: \x1b[33m${theWinner}!\x1b[0m` };
|
|
151
|
+
broadcastData(encrypt(JSON.stringify(broadPld)));
|
|
152
|
+
updatePrompt();
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (data.type === "typing") {
|
|
158
|
+
if (data.to && data.to !== username) return;
|
|
159
|
+
if (typingUsers.has(data.from)) clearTimeout(typingUsers.get(data.from));
|
|
160
|
+
|
|
161
|
+
typingUsers.set(data.from, setTimeout(() => {
|
|
162
|
+
typingUsers.delete(data.from);
|
|
163
|
+
updatePrompt();
|
|
164
|
+
}, 3000));
|
|
165
|
+
updatePrompt();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typingUsers.has(data.from)) {
|
|
170
|
+
clearTimeout(typingUsers.get(data.from));
|
|
171
|
+
typingUsers.delete(data.from);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!data.text) return;
|
|
175
|
+
if (data.type === "message" && data.to && data.to !== username) return;
|
|
176
|
+
|
|
177
|
+
readline.clearLine(process.stdout, 0);
|
|
178
|
+
readline.cursorTo(process.stdout, 0);
|
|
179
|
+
const senderEmoji = data.from === "Sistema" ? "🖥️" : getUserEmoji(data.from);
|
|
180
|
+
|
|
181
|
+
let dTxt = data.blink ? `\x1b[5m${data.text}\x1b[0m` : data.text;
|
|
182
|
+
if (data.to) { console.log(`🔒 [${senderEmoji} ${data.from} → tú]: ${dTxt}`); }
|
|
183
|
+
else { console.log(`🌍 [${senderEmoji} ${data.from}]: ${dTxt}`); }
|
|
184
|
+
updatePrompt();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function broadcastData(textPayload) {
|
|
188
|
+
const buffer = Buffer.from(textPayload);
|
|
189
|
+
if (mode === 'cloud') {
|
|
190
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
191
|
+
socket.send(buffer);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
socket.send(buffer, 0, buffer.length, PORT, BROADCAST_IP);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// == ESTABLECER CONEXION ==
|
|
199
|
+
if (mode === 'cloud') {
|
|
200
|
+
console.log("☁️ Intentando conectar a Cloudflare...");
|
|
201
|
+
socket = new WebSocket(CLOUD_URL);
|
|
202
|
+
socket.on("open", () => {
|
|
203
|
+
console.log("✅ Conectado a Servidor Global Cloudflare!");
|
|
204
|
+
initChatUI();
|
|
205
|
+
});
|
|
206
|
+
socket.on("message", (msg) => {
|
|
207
|
+
processMessage(msg.toString());
|
|
208
|
+
});
|
|
209
|
+
socket.on("error", (e) => console.log("⚠️ Error WS:", e.message));
|
|
210
|
+
} else {
|
|
211
|
+
socket = dgram.createSocket("udp4");
|
|
212
|
+
socket.on("listening", () => {
|
|
213
|
+
socket.setBroadcast(true);
|
|
214
|
+
console.log("📡 Red P2P Local Activa.");
|
|
215
|
+
initChatUI();
|
|
216
|
+
});
|
|
217
|
+
socket.on("message", (msg, rinfo) => {
|
|
218
|
+
if (getLocalIps().includes(rinfo.address)) return;
|
|
219
|
+
processMessage(msg.toString());
|
|
220
|
+
});
|
|
221
|
+
socket.on("error", (e) => console.log("⚠️ Error UDP:", e.message));
|
|
222
|
+
socket.bind(PORT);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let typingInterval;
|
|
226
|
+
let heartbeatInterval;
|
|
227
|
+
let pruneInterval;
|
|
228
|
+
let isClosing = false;
|
|
229
|
+
|
|
230
|
+
function cleanupAndExit() {
|
|
231
|
+
if (isClosing) return;
|
|
232
|
+
isClosing = true;
|
|
233
|
+
|
|
234
|
+
clearInterval(typingInterval);
|
|
235
|
+
clearInterval(heartbeatInterval);
|
|
236
|
+
clearInterval(pruneInterval);
|
|
237
|
+
clearInterval(animationInterval);
|
|
238
|
+
|
|
239
|
+
if (!username) {
|
|
240
|
+
if (mode === 'cloud') {
|
|
241
|
+
if (socket.readyState === WebSocket.OPEN) socket.close();
|
|
242
|
+
} else socket.close();
|
|
243
|
+
rl.close();
|
|
244
|
+
if (onExit) onExit();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const lvPayload = encrypt(JSON.stringify({ type: "leave", from: username }));
|
|
249
|
+
broadcastData(lvPayload);
|
|
250
|
+
|
|
251
|
+
setTimeout(() => {
|
|
252
|
+
if (mode === 'cloud') {
|
|
253
|
+
if (socket.readyState === WebSocket.OPEN) socket.close();
|
|
254
|
+
} else socket.close();
|
|
255
|
+
rl.close();
|
|
256
|
+
if (onExit) onExit();
|
|
257
|
+
}, 200);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
rl.on("line", (line) => {
|
|
261
|
+
if (!username) return;
|
|
262
|
+
const input = line.trim();
|
|
263
|
+
|
|
264
|
+
if (input === "/exit") { cleanupAndExit(); return; }
|
|
265
|
+
if (input === "/users") {
|
|
266
|
+
console.log("──────────── 👥 USUARIOS CONECTADOS ────────────");
|
|
267
|
+
const usersArr = Array.from(knownUsers);
|
|
268
|
+
if (usersArr.length === 0) console.log(" 👻 No hay otros usuarios todavía.");
|
|
269
|
+
else usersArr.forEach(u => console.log(` ${getUserEmoji(u)} ${u}`));
|
|
270
|
+
console.log(` (Tú) ${getUserEmoji(username)} ${username}`);
|
|
271
|
+
updatePrompt();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (input === "/clear") { console.clear(); updatePrompt(); return; }
|
|
275
|
+
if (input === "/help") {
|
|
276
|
+
console.log("──────────── 💡 COMANDOS DISPONIBLES ────────────");
|
|
277
|
+
console.log(" 💬 /chat <usuario> → Iniciar chat privado");
|
|
278
|
+
console.log(" 🌍 /all → Volver al chat público");
|
|
279
|
+
console.log(" 🪙 /flip → Lanzar una moneda");
|
|
280
|
+
console.log(" 🎮 /rps <usr> <j> → Retar a Piedra/Papel/Tijera");
|
|
281
|
+
console.log(" 🌟 /blink <msj> → Enviar msj parpadeante");
|
|
282
|
+
console.log(" ❌ /exit → Salir al menú");
|
|
283
|
+
updatePrompt();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (input === "/all") { currentChat = ""; console.log("🌍 Chat público"); updatePrompt(); return; }
|
|
287
|
+
if (input.startsWith("/chat ")) { currentChat = input.replace("/chat ", "").trim(); console.log(`🔒 Chat privado con ${currentChat}`); updatePrompt(); return; }
|
|
288
|
+
|
|
289
|
+
if (input === "/flip") {
|
|
290
|
+
const choices = ["CARA", "ESCUDO"];
|
|
291
|
+
const result = choices[Math.floor(Math.random() * choices.length)];
|
|
292
|
+
console.log(`🪙 Lanzaste la moneda y salió: \x1b[1m\x1b[33m${result}\x1b[0m`);
|
|
293
|
+
broadcastData(encrypt(JSON.stringify({ type: "flip", from: username, result })));
|
|
294
|
+
updatePrompt();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (input.startsWith("/rps ")) {
|
|
299
|
+
const parts = input.split(" ");
|
|
300
|
+
if (parts.length < 3) { console.log("❌ Usa: /rps <usuario> <piedra|papel|tijera>"); updatePrompt(); return; }
|
|
301
|
+
|
|
302
|
+
const targetUser = parts[1];
|
|
303
|
+
const jugada = parts[2].toLowerCase();
|
|
304
|
+
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; }
|
|
306
|
+
|
|
307
|
+
if (incomingRPS.has(targetUser)) {
|
|
308
|
+
incomingRPS.delete(targetUser);
|
|
309
|
+
console.log(`🎮 Respuesta enviada (${jugada})...`);
|
|
310
|
+
broadcastData(encrypt(JSON.stringify({ type: "rps_response", from: username, to: targetUser, choice: jugada })));
|
|
311
|
+
} else {
|
|
312
|
+
pendingRPS.set(targetUser, { miJugada: jugada, time: Date.now() });
|
|
313
|
+
console.log(`🎮 Retaste a ${targetUser} con ${jugada}...`);
|
|
314
|
+
broadcastData(encrypt(JSON.stringify({ type: "rps_challenge", from: username, to: targetUser })));
|
|
315
|
+
}
|
|
316
|
+
updatePrompt();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (input.startsWith("/") && !input.startsWith("/blink ")) {
|
|
321
|
+
console.log("❌ Comando no reconocido.");
|
|
322
|
+
updatePrompt();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let textToSend = input;
|
|
327
|
+
let isBlink = false;
|
|
328
|
+
if (input.startsWith("/blink ")) {
|
|
329
|
+
isBlink = true;
|
|
330
|
+
textToSend = input.replace("/blink ", "").trim();
|
|
331
|
+
if (!textToSend) { updatePrompt(); return; }
|
|
332
|
+
} else if (!input) { updatePrompt(); return; }
|
|
333
|
+
|
|
334
|
+
broadcastData(encrypt(JSON.stringify({ type: "message", from: username, to: currentChat || null, text: textToSend, blink: isBlink })));
|
|
335
|
+
|
|
336
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
337
|
+
readline.clearLine(process.stdout, 0);
|
|
338
|
+
|
|
339
|
+
const myEmoji = getUserEmoji(username);
|
|
340
|
+
let txtView = isBlink ? `\x1b[5m${textToSend}\x1b[0m` : textToSend;
|
|
341
|
+
|
|
342
|
+
if (currentChat) { console.log(`🔒 [Tú ${myEmoji} → ${getUserEmoji(currentChat)} ${currentChat}]: ${txtView}`); }
|
|
343
|
+
else { console.log(`🌍 [Tú ${myEmoji}]: ${txtView}`); }
|
|
344
|
+
updatePrompt();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
let lastTypedLine = "";
|
|
348
|
+
typingInterval = setInterval(() => {
|
|
349
|
+
if (username && rl.line && rl.line !== lastTypedLine && !rl.line.startsWith("/")) {
|
|
350
|
+
broadcastData(encrypt(JSON.stringify({ type: "typing", from: username, to: currentChat || null })));
|
|
351
|
+
}
|
|
352
|
+
lastTypedLine = rl.line || "";
|
|
353
|
+
}, 1500);
|
|
354
|
+
|
|
355
|
+
heartbeatInterval = setInterval(() => {
|
|
356
|
+
if (username) {
|
|
357
|
+
broadcastData(encrypt(JSON.stringify({ type: "heartbeat", from: username })));
|
|
358
|
+
}
|
|
359
|
+
}, 3000); // 3 seconds heartbeat for WebSocket liveness
|
|
360
|
+
|
|
361
|
+
pruneInterval = setInterval(() => {
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
for (const [user, time] of lastSeen.entries()) {
|
|
364
|
+
if (now - time > 15000) {
|
|
365
|
+
knownUsers.delete(user);
|
|
366
|
+
lastSeen.delete(user);
|
|
367
|
+
incomingRPS.delete(user);
|
|
368
|
+
pendingRPS.delete(user);
|
|
369
|
+
readline.clearLine(process.stdout, 0);
|
|
370
|
+
readline.cursorTo(process.stdout, 0);
|
|
371
|
+
console.log(`🔴 [${getUserEmoji(user)} ${user}] se ha desconectado.`);
|
|
372
|
+
updatePrompt();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}, 5000);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
module.exports = { startChat };
|
package/src/games/rps.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const readline = require("readline");
|
|
2
|
+
|
|
3
|
+
function startRPS(onExit) {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
console.clear();
|
|
10
|
+
console.log("=====================================");
|
|
11
|
+
console.log(" 🎮 PIEDRA, PAPEL O TIJERA 🎮");
|
|
12
|
+
console.log("=====================================");
|
|
13
|
+
console.log("Escribe 'piedra', 'papel' o 'tijera' para jugar.");
|
|
14
|
+
console.log("Escribe 'salir' para volver al menú principal.");
|
|
15
|
+
console.log("=====================================");
|
|
16
|
+
|
|
17
|
+
const options = ["piedra", "papel", "tijera"];
|
|
18
|
+
|
|
19
|
+
function play() {
|
|
20
|
+
rl.question("\nTu jugada: ", (answer) => {
|
|
21
|
+
const userChoice = answer.trim().toLowerCase();
|
|
22
|
+
|
|
23
|
+
if (userChoice === "salir" || userChoice === "/exit") {
|
|
24
|
+
rl.close();
|
|
25
|
+
if (onExit) onExit();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!options.includes(userChoice)) {
|
|
30
|
+
console.log("❌ Opción inválida. Usa: piedra, papel, tijera o salir.");
|
|
31
|
+
return play();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const botChoice = options[Math.floor(Math.random() * options.length)];
|
|
35
|
+
console.log(`🤖 La IA eligió: ${botChoice}`);
|
|
36
|
+
|
|
37
|
+
if (userChoice === botChoice) {
|
|
38
|
+
console.log("😐 ¡Es un empate!");
|
|
39
|
+
} else if (
|
|
40
|
+
(userChoice === "piedra" && botChoice === "tijera") ||
|
|
41
|
+
(userChoice === "papel" && botChoice === "piedra") ||
|
|
42
|
+
(userChoice === "tijera" && botChoice === "papel")
|
|
43
|
+
) {
|
|
44
|
+
console.log("🎉 ¡Ganaste!");
|
|
45
|
+
} else {
|
|
46
|
+
console.log("💀 ¡Perdiste!");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
play();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
play();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { startRPS };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const readline = require("readline");
|
|
2
|
+
const { startChat } = require("./chat/index.js");
|
|
3
|
+
const { startRPS } = require("./games/rps.js");
|
|
4
|
+
|
|
5
|
+
function showMenu() {
|
|
6
|
+
const rl = readline.createInterface({
|
|
7
|
+
input: process.stdin,
|
|
8
|
+
output: process.stdout
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
console.clear();
|
|
12
|
+
console.log("=====================================");
|
|
13
|
+
console.log(" 🚀 MULTI-HERRAMIENTA CLI 🚀");
|
|
14
|
+
console.log("=====================================");
|
|
15
|
+
console.log("1. Entrar al Chat P2P (Red Local/UDP)");
|
|
16
|
+
console.log("2. Entrar al Chat Global (Cloudflare/WS)");
|
|
17
|
+
console.log("3. Jugar Piedra, Papel o Tijera (Local)");
|
|
18
|
+
console.log("4. Salir");
|
|
19
|
+
console.log("=====================================");
|
|
20
|
+
|
|
21
|
+
rl.question("Elige una opción: ", (opcion) => {
|
|
22
|
+
const choice = opcion.trim();
|
|
23
|
+
rl.close(); // Cerramos esta instancia para que las apps usen las suyas
|
|
24
|
+
|
|
25
|
+
switch(choice) {
|
|
26
|
+
case "1":
|
|
27
|
+
startChat(showMenu, "udp");
|
|
28
|
+
break;
|
|
29
|
+
case "2":
|
|
30
|
+
startChat(showMenu, "cloud");
|
|
31
|
+
break;
|
|
32
|
+
case "3":
|
|
33
|
+
startRPS(showMenu);
|
|
34
|
+
break;
|
|
35
|
+
case "4":
|
|
36
|
+
console.log("👋 ¡Hasta luego!");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
console.log("❌ Opción inválida.");
|
|
41
|
+
setTimeout(showMenu, 1000);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Iniciar aplicación
|
|
48
|
+
showMenu();
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
const WebSocket = require("ws");
|
|
2
|
+
|
|
3
|
+
const PORT = process.env.PORT || 3000;
|
|
4
|
+
const wss = new WebSocket.Server({ port: PORT });
|
|
5
|
+
|
|
6
|
+
let globalTienditaTimer = null;
|
|
7
|
+
|
|
8
|
+
const sessions = new Map(); // socket -> { name: string, emoji: string }
|
|
9
|
+
const pendingRPS = new Map(); // targetName -> { from: name, miJugada: "piedra" }
|
|
10
|
+
|
|
11
|
+
const userEmojis = ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷", "🐸", "🐵", "🦄", "🐙", "🦋", "🦖", "🐧", "🦉", "👽", "🤖", "👻", "🥑", "🍕"];
|
|
12
|
+
|
|
13
|
+
function getTime() {
|
|
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
|
+
}
|
|
19
|
+
|
|
20
|
+
function broadcast(msg, ignoreWs = null) {
|
|
21
|
+
for (const [s, data] of sessions.entries()) {
|
|
22
|
+
if (s !== ignoreWs && s.readyState === WebSocket.OPEN && data.name) {
|
|
23
|
+
try { s.send(msg); } catch (e) { }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sendToTarget(name, msg) {
|
|
29
|
+
for (const [s, data] of sessions.entries()) {
|
|
30
|
+
if (data.name === name && s.readyState === WebSocket.OPEN) {
|
|
31
|
+
try { s.send(msg); } catch (e) { }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function targetExists(name) {
|
|
37
|
+
for (const data of sessions.values()) if (data.name === name) return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
wss.on("connection", (server) => {
|
|
42
|
+
sessions.set(server, { name: null, emoji: null });
|
|
43
|
+
|
|
44
|
+
server.send("\x1b[36m====================================\x1b[0m\n 🚀 BIENVENIDO A LA SALA GLOBAL 🚀\n\x1b[36m====================================\x1b[0m");
|
|
45
|
+
// 1. Al entrar por primera vez no es necesario /name
|
|
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) 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;
|
|
89
|
+
|
|
90
|
+
if (text === "/help") {
|
|
91
|
+
server.send(`\n──────────── 💡 COMANDOS MÁGICOS ────────────
|
|
92
|
+
👥 /users → Ver conectados
|
|
93
|
+
🪙 /flip → Lanzar moneda (Animado)
|
|
94
|
+
🎮 /rps <usr> <j> → Piedra-Papel-Tijera
|
|
95
|
+
🌟 /blink <msj> → Mensaje parpadeante
|
|
96
|
+
🔒 /w <usr> <msj> → Mensaje privado
|
|
97
|
+
🏪 /tiendita → Animación ASCII Sorpresa
|
|
98
|
+
ℹ️ /help → Esta ayuda
|
|
99
|
+
─────────────────────────────────────────────\n`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (text === "/users") {
|
|
104
|
+
let userList = [];
|
|
105
|
+
for (const d of sessions.values()) if (d.name) userList.push(`${d.emoji} ${d.name}`);
|
|
106
|
+
server.send(`\n👥 \x1b[36mCONECTADOS AHORA (${userList.length}):\x1b[0m ` + userList.join(", ") + "\n");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Indicador de "Escribiendo" Exclusivo para Smart Clients
|
|
111
|
+
if (text === "/typ") {
|
|
112
|
+
for (const [s, data] of sessions.entries()) {
|
|
113
|
+
if (s !== server && s.readyState === WebSocket.OPEN && data.smart) {
|
|
114
|
+
try { s.send(`/typ_event ${myEmoji} ${myName}`); } catch(e) {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return; // No lo procesamos más, es invisible
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Mensajes Privados
|
|
121
|
+
if (text.startsWith("/w ")) {
|
|
122
|
+
const parts = text.split(" ");
|
|
123
|
+
if (parts.length < 3) { server.send("❌ Error. Uso: /w <usuario> <mensaje secreto>"); return; }
|
|
124
|
+
const targetUser = parts[1];
|
|
125
|
+
const secretMsg = parts.slice(2).join(" ");
|
|
126
|
+
|
|
127
|
+
if (!targetExists(targetUser)) { server.send("❌ Usuario no encontrado."); return; }
|
|
128
|
+
if (targetUser === myName) { server.send("❌ ¿Hablando contigo mismo?"); return; }
|
|
129
|
+
|
|
130
|
+
server.send(`\n[${getTime()}] 🔒 [Secret → ${targetUser}]: \x1b[35m${secretMsg}\x1b[0m`);
|
|
131
|
+
sendToTarget(targetUser, `\n[${getTime()}] 🔒 [${myEmoji} Secreto de ${myName}]: \x1b[35m${secretMsg}\x1b[0m\n`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Moneda Animada de 3 Segundos
|
|
136
|
+
if (text === "/flip") {
|
|
137
|
+
server.send(`\n[${getTime()}] 🪙 Alistando la moneda...`);
|
|
138
|
+
broadcast(`\n[${getTime()}] 🪙 [${myEmoji} ${myName}] prepara un lanzamiento de moneda...\n`, server);
|
|
139
|
+
|
|
140
|
+
let count = 3;
|
|
141
|
+
const timer = setInterval(() => {
|
|
142
|
+
if (count > 0) {
|
|
143
|
+
const countMsg = ` ⏳ Girando en el aire... ${count}s`;
|
|
144
|
+
server.send(countMsg);
|
|
145
|
+
broadcast(countMsg, server);
|
|
146
|
+
count--;
|
|
147
|
+
} else {
|
|
148
|
+
clearInterval(timer);
|
|
149
|
+
const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
|
|
150
|
+
const renderMsg = `\n[${getTime()}] 🎯 ¡CAYÓ LA MONEDA de ${myName}! Es -> \x1b[1m\x1b[33m${result}\x1b[0m\n`;
|
|
151
|
+
server.send(renderMsg);
|
|
152
|
+
broadcast(renderMsg, server);
|
|
153
|
+
}
|
|
154
|
+
}, 1000);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Comando Animado ASCII de Cambios de Colores
|
|
159
|
+
if (text === "/tiendita") {
|
|
160
|
+
const introMsg = `\n[${getTime()}] 🏪 \x1b[1m${myName}\x1b[0m convoca a todos a la tiendita...\n`;
|
|
161
|
+
server.send(introMsg);
|
|
162
|
+
broadcast(introMsg, server);
|
|
163
|
+
|
|
164
|
+
const asciiArt = ` _____ _ _ _ _ _____ _ _ _ _
|
|
165
|
+
|_ _(_) | (_) | |_ _(_) | | | |
|
|
166
|
+
| | _ ___ _ __ __| |_| |_ __ _ | | _ _ __ ___ ___| | | |
|
|
167
|
+
| | | |/ _ \\ '_ \\ / _\` | | __/ _\` | | | | | '_ \` _ \\ / _ \\ | | |
|
|
168
|
+
| | | | __/ | | | (_| | | || (_| | | | | | | | | | | __/_|_|_|
|
|
169
|
+
\\_/ |_|\\___|_| |_|\\__,_|_|\\__\\__,_| \\_/ |_|_| |_| |_|\\___(_|_|_)`;
|
|
170
|
+
|
|
171
|
+
const baseColors = [
|
|
172
|
+
"\x1b[31m", // Rojo
|
|
173
|
+
"\x1b[33m", // Amarillo
|
|
174
|
+
"\x1b[32m", // Verde
|
|
175
|
+
"\x1b[36m", // Cyan
|
|
176
|
+
"\x1b[1m\x1b[35m" // Magenta Brillante
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
// Primer frame puro sin código de arrastre hacia arriba
|
|
180
|
+
const f0 = `${baseColors[0]}${asciiArt}\x1b[0m`;
|
|
181
|
+
server.send(f0);
|
|
182
|
+
broadcast(f0, server);
|
|
183
|
+
|
|
184
|
+
let i = 1;
|
|
185
|
+
globalTienditaTimer = setInterval(() => {
|
|
186
|
+
const color = baseColors[i % baseColors.length];
|
|
187
|
+
const frame = `\r\x1b[6A\x1b[0J< ${color}${asciiArt}\x1b[0m`;
|
|
188
|
+
server.send(frame);
|
|
189
|
+
broadcast(frame, server);
|
|
190
|
+
i++;
|
|
191
|
+
}, 600); // Bucle Infinito a 600ms
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (text.startsWith("/blink ")) {
|
|
196
|
+
const blnkText = text.replace("/blink ", "").trim();
|
|
197
|
+
const msg = `\n[${getTime()}] 🌟 [${myEmoji} ${myName}]: \x1b[5m${blnkText}\x1b[0m\n`;
|
|
198
|
+
server.send(msg);
|
|
199
|
+
broadcast(msg, server);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (text.startsWith("/rps ")) {
|
|
204
|
+
const parts = text.split(" ");
|
|
205
|
+
if (parts.length < 3) { server.send("❌ Error: /rps <usuario> <piedra|papel|tijera>"); return; }
|
|
206
|
+
const target = parts[1];
|
|
207
|
+
const choice = parts[2].toLowerCase();
|
|
208
|
+
|
|
209
|
+
if (!["piedra", "papel", "tijera"].includes(choice)) { server.send("❌ Elige piedra, papel o tijera."); return; }
|
|
210
|
+
if (target === myName) { server.send("❌ No contra ti mismo."); return; }
|
|
211
|
+
if (!targetExists(target)) { server.send(`❌ El usuario ${target} no existe.`); return; }
|
|
212
|
+
|
|
213
|
+
if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
|
|
214
|
+
const retoAnterior = pendingRPS.get(myName);
|
|
215
|
+
pendingRPS.delete(myName);
|
|
216
|
+
|
|
217
|
+
const miJugada = choice;
|
|
218
|
+
const suJugada = retoAnterior.miJugada;
|
|
219
|
+
|
|
220
|
+
let winStr;
|
|
221
|
+
if (miJugada === suJugada) { winStr = "😐 Empate"; }
|
|
222
|
+
else if ((miJugada === "piedra" && suJugada === "tijera") || (miJugada === "papel" && suJugada === "piedra") || (miJugada === "tijera" && suJugada === "papel")) { winStr = `🎉 \x1b[32m${myName}!\x1b[0m`; }
|
|
223
|
+
else { winStr = `🎉 \x1b[32m${target}!\x1b[0m`; }
|
|
224
|
+
|
|
225
|
+
const outMsg = `\n[${getTime()}] 🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${suJugada}) VS \x1b[36m${myName}\x1b[0m (${miJugada})\n🏆 Ganador: ${winStr}\n`;
|
|
226
|
+
server.send(outMsg);
|
|
227
|
+
broadcast(outMsg, server);
|
|
228
|
+
} else {
|
|
229
|
+
pendingRPS.set(target, { from: myName, miJugada: choice });
|
|
230
|
+
server.send(`\n[${getTime()}] 🎮 \x1b[33mTu elección escondida (${choice}) ha sido fijada. Esperando a ${target}.\x1b[0m`);
|
|
231
|
+
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`);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (text.startsWith("/")) {
|
|
237
|
+
server.send("❌ Desconocido. Usa /help");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Chat
|
|
242
|
+
const msg = `\n[${getTime()}] 🌍 [${myEmoji} ${myName}]: ${text}`;
|
|
243
|
+
server.send(msg);
|
|
244
|
+
broadcast(msg, server);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
server.on("close", () => {
|
|
248
|
+
const u = sessions.get(server);
|
|
249
|
+
if (u && u.name) broadcast(`\n[${getTime()}] 🔴 [\x1b[31m${u.emoji} ${u.name}\x1b[0m] abandonó la sala.\n`, server);
|
|
250
|
+
sessions.delete(server);
|
|
251
|
+
pendingRPS.delete(u ? u.name : "");
|
|
252
|
+
for (const [t, data] of pendingRPS.entries()) {
|
|
253
|
+
if (data.from === u?.name) pendingRPS.delete(t);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
server.on("error", () => {
|
|
258
|
+
sessions.delete(server);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
console.log(`📡 Servidor WebSocket de Node corriendo en puerto ${PORT}`);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const sessions = new Map(); // socket -> { name: string }
|
|
2
|
+
const pendingRPS = new Map(); // targetName -> { from: name, miJugada: "piedra" }
|
|
3
|
+
|
|
4
|
+
const userEmojis = ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷", "🐸", "🐵", "🦄", "🐙", "🦋", "🦖", "🐧", "🦉", "👽", "🤖", "👻", "🥑", "🍕"];
|
|
5
|
+
|
|
6
|
+
function getUserEmoji(name) {
|
|
7
|
+
if (!name) return "👤";
|
|
8
|
+
let hash = 0;
|
|
9
|
+
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
10
|
+
const index = Math.abs(hash) % userEmojis.length;
|
|
11
|
+
return userEmojis[index];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function broadcast(msg, ignoreWs = null) {
|
|
15
|
+
for (const [s, data] of sessions.entries()) {
|
|
16
|
+
if (s !== ignoreWs && data.name) {
|
|
17
|
+
try { s.send(msg); } catch(e) {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sendToTarget(name, msg) {
|
|
23
|
+
for (const [s, data] of sessions.entries()) {
|
|
24
|
+
if (data.name === name) {
|
|
25
|
+
try { s.send(msg); } catch(e) {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
async fetch(request, env) {
|
|
32
|
+
const upgradeHeader = request.headers.get('Upgrade');
|
|
33
|
+
if (!upgradeHeader || upgradeHeader !== 'websocket') {
|
|
34
|
+
return new Response('Expected Upgrade: websocket.', { status: 426 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const webSocketPair = new WebSocketPair();
|
|
38
|
+
const [client, server] = Object.values(webSocketPair);
|
|
39
|
+
|
|
40
|
+
server.accept();
|
|
41
|
+
sessions.set(server, { name: null });
|
|
42
|
+
|
|
43
|
+
server.send("\x1b[36m====================================\x1b[0m\n 🚀 BIENVENIDO A LA NUBE CLOUDFLARE 🚀\n\x1b[36m====================================\x1b[0m");
|
|
44
|
+
server.send("🤖 Sistema: Por favor, danos tu nombre. (escribe \x1b[33m/name TuNombre\x1b[0m para entrar):");
|
|
45
|
+
|
|
46
|
+
server.addEventListener('message', event => {
|
|
47
|
+
const socketData = sessions.get(server);
|
|
48
|
+
let text = event.data;
|
|
49
|
+
if (typeof text !== 'string') text = text.toString();
|
|
50
|
+
text = text.trim();
|
|
51
|
+
|
|
52
|
+
if (!socketData.name) {
|
|
53
|
+
if (text.startsWith("/name ")) {
|
|
54
|
+
const desiredName = text.replace("/name ", "").trim();
|
|
55
|
+
let alreadyExists = false;
|
|
56
|
+
for (const d of sessions.values()) if (d.name === desiredName) alreadyExists = true;
|
|
57
|
+
|
|
58
|
+
if (alreadyExists) {
|
|
59
|
+
server.send("❌ Ese nombre ya está en uso. Usa /name con otro.");
|
|
60
|
+
} else {
|
|
61
|
+
socketData.name = desiredName;
|
|
62
|
+
server.send(`✅ ¡Hola ${desiredName}! Acabas de entrar. Usa /help para ver tus comandos.`);
|
|
63
|
+
broadcast(`\n🟢 [\x1b[32m${getUserEmoji(desiredName)} ${desiredName}\x1b[0m] ha entrado a la sala.\n`, server);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
server.send("⚠️ Aún no tienes nombre registrado. Escribe: /name Usuario");
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const myName = socketData.name;
|
|
72
|
+
const myEmoji = getUserEmoji(myName);
|
|
73
|
+
|
|
74
|
+
if (text === "/help") {
|
|
75
|
+
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
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (text === "/users") {
|
|
80
|
+
let userList = [];
|
|
81
|
+
for (const d of sessions.values()) if (d.name) userList.push(`${getUserEmoji(d.name)} ${d.name}`);
|
|
82
|
+
server.send("\n👥 \x1b[36mCONECTADOS AHORA:\x1b[0m " + userList.join(", ") + "\n");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (text === "/flip") {
|
|
87
|
+
const result = Math.random() < 0.5 ? "CARA" : "ESCUDO";
|
|
88
|
+
const msg = `\n🪙 [${myEmoji} ${myName}] ha lanzado la moneda y cayó en: \x1b[1m\x1b[33m${result}\x1b[0m!\n`;
|
|
89
|
+
server.send(msg); // <--- ECHO LOCAL agregado
|
|
90
|
+
broadcast(msg, server);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (text.startsWith("/blink ")) {
|
|
95
|
+
const blnkText = text.replace("/blink ", "").trim();
|
|
96
|
+
const msg = `\n🌟 [${myEmoji} ${myName}]: \x1b[5m${blnkText}\x1b[0m\n`;
|
|
97
|
+
server.send(msg); // <--- ECHO LOCAL agregado
|
|
98
|
+
broadcast(msg, server);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (text.startsWith("/rps ")) {
|
|
103
|
+
const parts = text.split(" ");
|
|
104
|
+
if (parts.length < 3) { server.send("❌ Error: /rps <usuario> <piedra|papel|tijera>"); return; }
|
|
105
|
+
const target = parts[1];
|
|
106
|
+
const choice = parts[2].toLowerCase();
|
|
107
|
+
|
|
108
|
+
if (!["piedra", "papel", "tijera"].includes(choice)) { server.send("❌ Elige piedra, papel o tijera."); return; }
|
|
109
|
+
if (target === myName) { server.send("❌ No contra ti mismo."); return; }
|
|
110
|
+
|
|
111
|
+
if (pendingRPS.has(myName) && pendingRPS.get(myName).from === target) {
|
|
112
|
+
const retoAnterior = pendingRPS.get(myName);
|
|
113
|
+
pendingRPS.delete(myName);
|
|
114
|
+
|
|
115
|
+
const miJugada = choice;
|
|
116
|
+
const suJugada = retoAnterior.miJugada;
|
|
117
|
+
|
|
118
|
+
let winStr;
|
|
119
|
+
if (miJugada === suJugada) { winStr = "😐 Empate"; }
|
|
120
|
+
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${target}!\x1b[0m`; }
|
|
122
|
+
|
|
123
|
+
const outMsg = `\n🕹️ RESULTADO RPS 🕹️\n⚔️ \x1b[36m${target}\x1b[0m (${suJugada}) VS \x1b[36m${myName}\x1b[0m (${miJugada})\n🏆 Ganador: ${winStr}\n`;
|
|
124
|
+
server.send(outMsg); // ECHO LOCAL
|
|
125
|
+
broadcast(outMsg, server);
|
|
126
|
+
} else {
|
|
127
|
+
pendingRPS.set(target, { from: myName, miJugada: choice });
|
|
128
|
+
server.send(`\n🎮 \x1b[33mTu reto (${choice}) ha sido enviado a ${target}.\x1b[0m`);
|
|
129
|
+
sendToTarget(target, `\n\x1b[5m\x1b[35m🚨 ¡${myName} te reta a RPS!\x1b[0m\n👉 Responde: /rps ${myName} <piedra|papel|tijera>\n`);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (text.startsWith("/")) {
|
|
135
|
+
server.send("❌ Comando no reconocido. Usa /help");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Comunicación general
|
|
140
|
+
const msg = `\n🌍 [${myEmoji} ${myName}]: ${text}`;
|
|
141
|
+
server.send(msg); // <----- ECHO LOCAL AGREGADO PARA QUE LEAS TUS PROPIOS MSJS EN WSCAT
|
|
142
|
+
broadcast(msg, server);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
server.addEventListener('close', () => {
|
|
146
|
+
const u = sessions.get(server);
|
|
147
|
+
if (u && u.name) broadcast(`\n🔴 [\x1b[31m${getUserEmoji(u.name)} ${u.name}\x1b[0m] abandonó la sala.\n`, server);
|
|
148
|
+
sessions.delete(server);
|
|
149
|
+
pendingRPS.delete(u ? u.name : "");
|
|
150
|
+
for(const [t, data] of pendingRPS.entries()) {
|
|
151
|
+
if(data.from === u?.name) pendingRPS.delete(t);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
server.addEventListener('error', () => {
|
|
156
|
+
sessions.delete(server);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return new Response(null, {
|
|
160
|
+
status: 101,
|
|
161
|
+
webSocket: client,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const CryptoJS = require("crypto-js");
|
|
2
|
+
const SECRET_KEY = new Date().toISOString().slice(0, 10);
|
|
3
|
+
|
|
4
|
+
function encrypt(text) {
|
|
5
|
+
return CryptoJS.AES.encrypt(text, SECRET_KEY).toString();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function decrypt(cipher) {
|
|
9
|
+
try {
|
|
10
|
+
const bytes = CryptoJS.AES.decrypt(cipher, SECRET_KEY);
|
|
11
|
+
return bytes.toString(CryptoJS.enc.Utf8);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { encrypt, decrypt };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const userEmojis = ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷", "🐸", "🐵", "🦄", "🐙", "🦋", "🦖", "🐧", "🦉", "👽", "🤖", "👻", "🥑", "🍕"];
|
|
2
|
+
|
|
3
|
+
function getUserEmoji(name) {
|
|
4
|
+
if (!name) return "👤";
|
|
5
|
+
let hash = 0;
|
|
6
|
+
for (let i = 0; i < name.length; i++) {
|
|
7
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
8
|
+
}
|
|
9
|
+
const index = Math.abs(hash) % userEmojis.length;
|
|
10
|
+
return userEmojis[index];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { getUserEmoji };
|
package/wrangler.toml
ADDED