diff --git a/app.js b/app.js index b6b7a5a..6e22396 100644 --- a/app.js +++ b/app.js @@ -32,7 +32,15 @@ const io = new Server(server); app.use( helmet({ - contentSecurityPolicy: false, + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", "data:"], + connectSrc: ["'self'", "ws:", "wss:"], + }, + }, }), ); @@ -125,6 +133,14 @@ io.on("connection", (socket) => { socket.on("register", (username) => { socket.user = username; onlineUsers[username] = socket.id; + io.emit("onlineUsers", Object.keys(onlineUsers)); + }); + + socket.on("disconnect", () => { + if (socket.user) { + delete onlineUsers[socket.user]; + io.emit("onlineUsers", Object.keys(onlineUsers)); + } }); socket.on("chatMessage", (data) => { @@ -145,6 +161,30 @@ io.on("connection", (socket) => { } }); + socket.on("whisper", (data) => { + const targetSocket = onlineUsers[data.to]; + + if (!targetSocket) { + socket.emit("systemMessage", { + message: data.to + " ist offline", + }); + + return; + } + + io.to(targetSocket).emit("chatMessage", { + user: socket.user, + message: data.message, + channel: "private", + }); + + socket.emit("chatMessage", { + user: "(an " + data.to + ")", + message: data.message, + channel: "private", + }); + }); + socket.on("privateMessage", (data) => { const target = onlineUsers[data.to]; diff --git a/public/css/launcher.css b/public/css/launcher.css index f496046..cb58431 100644 --- a/public/css/launcher.css +++ b/public/css/launcher.css @@ -196,3 +196,44 @@ .private-chat { color: #7ec8ff; } + +#online-users { + position: fixed; + + left: 20px; + bottom: 340px; + + width: 350px; + + max-height: 200px; + + background: linear-gradient(#2b2115, #1a140d); + + border: 3px solid #8b6a3c; + + color: #f3e6c5; + + font-family: serif; + + overflow-y: auto; +} + +.online-header { + background: #3a2a17; + + padding: 5px; + + text-align: center; + + border-bottom: 2px solid #8b6a3c; +} + +#online-list div { + padding: 4px; + + cursor: pointer; +} + +#online-list div:hover { + background: #5b4325; +} diff --git a/public/js/chat.js b/public/js/chat.js index c6d90d4..b500275 100644 --- a/public/js/chat.js +++ b/public/js/chat.js @@ -1,4 +1,5 @@ const socket = io(); +socket.emit("register", window.playerName); let channel = "global"; @@ -40,10 +41,23 @@ document.getElementById("chat-send").onclick = () => { if (!text) return; - socket.emit("chatMessage", { - channel: channel, - message: text, - }); + if (text.startsWith("/w ")) { + const parts = text.split(" "); + + const target = parts[1]; + + const message = parts.slice(2).join(" "); + + socket.emit("whisper", { + to: target, + message: message, + }); + } else { + socket.emit("chatMessage", { + channel: channel, + message: text, + }); + } document.getElementById("chat-text").value = ""; }; @@ -57,3 +71,31 @@ socket.on("chatMessage", (data) => { if (data.channel === channel) updateChat(); }); + +socket.on("systemMessage", (data) => { + const line = "[System] " + data.message; + + history[channel].push(line); + + updateChat(); +}); + +socket.on("onlineUsers", (users) => { + const list = document.getElementById("online-list"); + + list.innerHTML = ""; + + users.forEach((user) => { + const div = document.createElement("div"); + + div.innerText = user; + + div.onclick = () => { + document.getElementById("chat-text").value = "/w " + user + " "; + + document.getElementById("chat-text").focus(); + }; + + list.appendChild(div); + }); +}); diff --git a/views/launcher.ejs b/views/launcher.ejs index b5e6f69..1945065 100644 --- a/views/launcher.ejs +++ b/views/launcher.ejs @@ -179,6 +179,12 @@ +