There are two commands in this room and those are /name <new name> and /report which will tell the administrator to come in and look at people talking about dogs. Remember: This room only allows chats regarding cats! Other than that the problem will give you the following server side code.
const http = require('http'); const express = require('express'); const cookieParser = require('cookie-parser') const uuidv4 = require('uuid/v4'); const SSEClient = require('sse').Client; const admin = require('./admin'); const pubsub = require('@google-cloud/pubsub')(); const app = express(); app.set('etag', false); app.use(cookieParser()); // Check if user is admin based on the 'flag' cookie, and set the 'admin' flag on the request object app.use(admin.middleware); // Check if banned app.use(function(req, res, next) { if (req.cookies.banned) { res.sendStatus(403); res.end(); } else { next(); } }); // Opening redirect and room index app.get('/', (req, res) => res.redirect(`/room/${uuidv4()}/`)); let roomPath = '/room/:room([0-9a-f-]{36})'; app.get(roomPath + '/', function(req, res) { res.sendFile(__dirname + '/static/index.html', { headers: { 'Content-Security-Policy': [ 'default-src \'self\'', 'style-src \'unsafe-inline\' \'self\'', 'script-src \'self\' www.google.com/recaptcha/www.gstatic.com/recaptcha/', 'frame-src \'self\' www.google.com/recaptcha/', ].join('; ') }, }); }); // Process incoming messages app.all(roomPath + '/send', async function(req, res) { let room = req.params.room, {msg, name} = req.query, response = {}, arg; console.log(`${room} <-- (${name}):`, msg) if (!(req.headers.referer || '').replace(/^https?:\/\//, '').startsWith(req.headers.host)) { response = {type: "error", error: 'CSRF protection error'}; } else if (msg[0] != '/') { broadcast(room, {type: 'msg', name, msg}); } else { switch (msg.match(/^\/[^ ]*/)[0]) { case '/name': if (!(arg = msg.match(/\/name (.+)/))) break; response = {type: 'rename', name: arg[1]}; broadcast(room, {type: 'name', name: arg[1], old: name}); case '/ban': if (!(arg = msg.match(/\/ban (.+)/))) break; if (!req.admin) break; broadcast(room, {type: 'ban', name: arg[1]}); case '/secret': if (!(arg = msg.match(/\/secret (.+)/))) break; res.setHeader('Set-Cookie', 'flag=' + arg[1] + '; Path=/; Max-Age=31536000'); response = {type: 'secret'}; case '/report': if (!(arg = msg.match(/\/report (.+)/))) break; var ip = req.headers['x-forwarded-for']; ip = ip ? ip.split(',')[0] : req.connection.remoteAddress; response = await admin.report(arg[1], ip, `https://${req.headers.host}/room/${room}/`); } } console.log(`${room} --> (${name}):`, response) res.json(response); res.status(200); res.end(); }); // Process room broadcast messages const rooms = new Map(); app.get(roomPath + '/receive', function(req, res) { res.setHeader('X-Accel-Buffering', 'no'); let channel = new SSEClient(req, res); channel.initialize(); let roomName = req.params.room; let room = rooms.get(roomName) || new Set(); rooms.set(roomName, room.add(channel)) req.once('close', () => { room.size > 1 ? room.delete(channel) : rooms.delete(roomName) }); }); // Broadcast to all instances using Cloud Pub/Sub. For local testing, it's easy // to skip by commenting it out and patching the broadcast fn below. var publisher; pubsub.createTopic('catchat', function() { var topic = pubsub.topic('catchat'); publisher = topic.publisher(); topic.createSubscription('catchat-' + uuidv4(), {ackDeadlineSeconds: 10}).then(function(data) { data[0].on('message', function(msg) { msg.ack(); var room = msg.attributes.room; if (!rooms.has(room)) return; var msg = msg.data.toString('utf-8'); console.log(`${room} ^^^`, msg) for (let channel of rooms.get(room)) channel.send(msg); }); }); }); function broadcast(room, msg) { // for (let channel of (rooms.get(room) || [])) channel.send(JSON.stringify(msg)); // Local broadcast only publisher.publish(Buffer.from(JSON.stringify(msg)), {room: room}); // Pub/Sub broadcast } // Static files app.get('/server.js', (req, res) => res.sendFile(__filename)); app.use(express.static(__dirname + '/static/', {fallthrough: false})); app.listen(8080);
The client-side code in HTML/JS looks like this:
// Set name let color = ['brown', 'black', 'yellow', 'white', 'grey', 'red'][Math.floor(Math.random()*6)]; let breed = ['ragamuffin', 'persian', 'siamese', 'siberian', 'birman', 'bombay', 'ragdoll'][Math.floor(Math.random()*7)]; if (!localStorage.name) localStorage.name = color + '_' + breed; // Utility functions let cookie = (name) => (document.cookie.match(new RegExp(`(?:^|; )${name}=(.*?)(?:$|;)`)) || [])[1]; let esc = (str) => str.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); // Sending messages let send = (msg) => fetch(`send?name=${encodeURIComponent(localStorage.name)}&msg=${encodeURIComponent(msg)}`, {credentials: 'include'}).then((res) => res.json()).then(handle); let display = (line) => conversation.insertAdjacentHTML('beforeend', `<p>${line}</p>`); let recaptcha_id = '6LeB410UAAAAAGkmQanWeqOdR6TACZTVypEEXHcu'; window.addEventListener('load', function() { messagebox.addEventListener('keydown', function(event) { if (event.keyCode == 13 && messagebox.value != '') { if (messagebox.value == '/report') { grecaptcha.execute(recaptcha_id, {action: 'report'}).then((token) => send('/report ' + token)); } else { send(messagebox.value); } messagebox.value = ''; } }); send('Hi all'); }); // Receiving messages function handle(data) { ({ undefined(data) {}, error(data) { display(`Something went wrong :/ Check the console for error message.`); console.error(data); }, name(data) { display(`${esc(data.old)} is now known as ${esc(data.name)}`); }, rename(data) { localStorage.name = data.name; }, secret(data) { display(`Successfully changed secret to <span data-secret="${esc(cookie('flag'))}">*****</span>`); }, msg(data) { let you = (data.name == localStorage.name) ? ' (you)' : ''; if (!you && data.msg == 'Hi all') send('Hi'); display(`<span data-name="${esc(data.name)}">${esc(data.name)}${you}</span>: <span>${esc(data.msg)}</span>`); }, ban(data) { if (data.name == localStorage.name) { document.cookie = 'banned=1; Path=/'; sse.close(); display(`You have been banned and from now on won't be able to receive and send messages.`); } else { display(`${esc(data.name)} was banned.<style>span[data-name^=${esc(data.name)}] { color: red; }</style>`); } }, })[data.type](data); } let sse = new EventSource("receive"); sse.onmessage = (msg) => handle(JSON.parse(msg.data)); // Say goodbye window.addEventListener('unload', () => navigator.sendBeacon(`send?name=${encodeURIComponent(localStorage.name)}&msg=Bye`)); // Admin helper function. Invoke this to automate banning people in a misbehaving room. // Note: the admin will already have their secret set in the cookie (it's a cookie with long expiration), // so no need to deal with /secret and such when joining a room. function cleanupRoomFullOfBadPeople() { send(`I've been notified that someone has brought up a forbidden topic. I will ruthlessly ban anyone who mentions d*gs going forward. Please just stop and start talking about cats for d*g's sake.`); last = conversation.lastElementChild; setInterval(function() { var p; while (p = last.nextElementSibling) { last = p; if (p.tagName != 'P' || p.children.length < 2) continue; var name = p.children[0].innerText; var msg = p.children[1].innerText; if (msg.match(/dog/i)) { send(`/ban ${name}`); send(`As I said, d*g talk will not be tolerated.`); } } }, 1000); }
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Cat Chat</title> <script src="/catchat.js"></script> <script src="https://www.google.com/recaptcha/api.js?render=6LeB410UAAAAAGkmQanWeqOdR6TACZTVypEEXHcu"></script> <link rel="stylesheet" type="text/css" href="/style.css"> </head> <body> <div id="panel"> <div id="conversation"> <p>Welcome to Cat Chat! This is your brand new room where you can discuss anything related to cats. You have been assigned a random nick name that you can change any time.</p> <p>Rules:</p> <p>- You may invite anyone to this chat room. Just share the URL.</p> <p>- Dog talk is strictly forbidden. If you see anyone talking about dogs, please report the incident, and the admin will take the appropriate steps. This usually means that the admin joins the room, listens to the conversation for a brief period and bans anyone who mentions dogs.</p> <p>Commands you can use: (just type a message starting with slash to invoke commands)</p> <p>- `/name YourNewName` - Change your nick name to YourNewName.</p> <p>- `/report` - Report dog talk to the admin.</p> <!-- Admin commands: - `/secret asdfg` - Sets the admin password to be sent to the server with each command for authentication. It's enough to set it once a year, so no need to issue a /secret command every time you open a chat room. - `/ban UserName` - Bans the user with UserName from the chat (requires the correct admin password to be set). --> <p>Btw, the core of the chat engine is open source! You can download the source code <a href="/server.js">here</a>.</p> <p style="margin-bottom: 5em">Alright, have fun!</p> </div> <input id="messagebox" autofocus> </div> </body> </html>