From c719020e17cb1c98da55be6cc7efe0e50ab51ffa Mon Sep 17 00:00:00 2001 From: marzavec Date: Sat, 29 Sep 2018 23:44:36 -0700 Subject: Added hooks, modules and cleaned up code --- server/src/commands/core/changenick.js | 30 ++++----- server/src/commands/core/chat.js | 90 +++++++++++++++++++------ server/src/commands/core/help.js | 95 ++++++++++++++++++-------- server/src/commands/core/invite.js | 18 ++--- server/src/commands/core/join.js | 119 +++++++++++++++++---------------- server/src/commands/core/morestats.js | 30 ++++++++- server/src/commands/core/move.js | 12 ++-- server/src/commands/core/ping.js | 2 + server/src/commands/core/stats.js | 6 +- server/src/commands/core/whisper.js | 110 ++++++++++++++++++++++++++++++ 10 files changed, 372 insertions(+), 140 deletions(-) create mode 100644 server/src/commands/core/whisper.js (limited to 'server/src/commands/core') diff --git a/server/src/commands/core/changenick.js b/server/src/commands/core/changenick.js index 5a8b5c2..6cc967c 100644 --- a/server/src/commands/core/changenick.js +++ b/server/src/commands/core/changenick.js @@ -1,17 +1,17 @@ /* - Description: Generates a semi-unique channel name then broadcasts it to each client + Description: Allows calling client to change their current nickname */ +// module support functions const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick); +// module main exports.run = async (core, server, socket, data) => { if (server._police.frisk(socket.remoteAddress, 6)) { - server.reply({ + return server.reply({ cmd: 'warn', text: 'You are changing nicknames too fast. Wait a moment before trying again.' }, socket); - - return; } // verify user data is string @@ -22,12 +22,10 @@ exports.run = async (core, server, socket, data) => { // make sure requested nickname meets standards let newNick = data.nick.trim(); if (!verifyNickname(newNick)) { - server.reply({ + return server.reply({ cmd: 'warn', text: 'Nickname must consist of up to 24 letters, numbers, and underscores' }, socket); - - return; } // prevent admin impersonation @@ -35,12 +33,10 @@ exports.run = async (core, server, socket, data) => { if (newNick.toLowerCase() == core.config.adminName.toLowerCase()) { server._police.frisk(socket.remoteAddress, 4); - server.reply({ + return server.reply({ cmd: 'warn', - text: 'Gtfo' + text: 'You are not the admin, liar!' }, socket); - - return; } // find any sockets that have the same nickname @@ -52,12 +48,10 @@ exports.run = async (core, server, socket, data) => { // return error if found if (userExists.length > 0) { // That nickname is already in that channel - server.reply({ + return server.reply({ cmd: 'warn', text: 'Nickname taken' }, socket); - - return; } // build join and leave notices @@ -65,6 +59,7 @@ exports.run = async (core, server, socket, data) => { cmd: 'onlineRemove', nick: socket.nick }; + let joinNotice = { cmd: 'onlineAdd', nick: newNick, @@ -86,10 +81,11 @@ exports.run = async (core, server, socket, data) => { socket.nick = newNick; }; +// module meta exports.requiredData = ['nick']; - exports.info = { name: 'changenick', - usage: 'changenick {nick}', - description: 'This will change your current connections nickname' + description: 'This will change your current connections nickname', + usage: ` + API: { cmd: 'changenick', nick: '' }` }; diff --git a/server/src/commands/core/chat.js b/server/src/commands/core/chat.js index 89d497e..70b9876 100644 --- a/server/src/commands/core/chat.js +++ b/server/src/commands/core/chat.js @@ -2,6 +2,7 @@ Description: Rebroadcasts any `text` to all clients in a `channel` */ +// module support functions const parseText = (text) => { // verifies user input is text if (typeof text !== 'string') { @@ -16,25 +17,23 @@ const parseText = (text) => { return text; }; +// module main exports.run = async (core, server, socket, data) => { // check user input let text = parseText(data.text); + if (!text) { // lets not send objects or empty text, yea? - server._police.frisk(socket.remoteAddress, 6); - - return; + return server._police.frisk(socket.remoteAddress, 13); } // check for spam let score = text.length / 83 / 4; if (server._police.frisk(socket.remoteAddress, score)) { - server.reply({ + return server.reply({ cmd: 'warn', text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.' }, socket); - - return; } // build chat payload @@ -54,26 +53,77 @@ exports.run = async (core, server, socket, data) => { payload.trip = socket.trip; } - // check if the users connection is muted - // TODO: Add a more contained way for modules to interact, event hooks or something? - if(core.muzzledHashes && core.muzzledHashes[socket.hash]){ - server.broadcast( payload, { channel: socket.channel, hash: socket.hash }); - if(core.muzzledHashes[socket.hash].allies){ - server.broadcast( payload, { channel: socket.channel, nick: core.muzzledHashes[socket.hash].allies }); - } - } else { - //else send it to everyone - server.broadcast( payload, { channel: socket.channel}); - } + // broadcast to channel peers + server.broadcast( payload, { channel: socket.channel}); // stats are fun core.managers.stats.increment('messages-sent'); }; -exports.requiredData = ['text']; +// module hook functions +exports.initHooks = (server) => { + server.registerHook('in', 'chat', this.commandCheckIn); + server.registerHook('out', 'chat', this.commandCheckOut); +}; + +// checks for miscellaneous '/' based commands +exports.commandCheckIn = (core, server, socket, payload) => { + if (typeof payload.text !== 'string') { + return false; + } + + if (payload.text.startsWith('/myhash')) { + server.reply({ + cmd: 'info', + text: `Your hash: ${socket.hash}` + }, socket); + + return false; + } + + return payload; +}; + +// checks for miscellaneous '/' based commands +exports.commandCheckOut = (core, server, socket, payload) => { + if (!payload.text.startsWith('/')) { + return payload; + } + + if (payload.text.startsWith('//me ')) { + payload.text = payload.text.substr(1, payload.text.length); + + return payload; + } + // TODO: make emotes their own module #lazydev + if (payload.text.startsWith('/me ')) { + let emote = payload.text.substr(4); + if (emote.trim() === '') { + emote = 'fails at life'; + } + + let newPayload = { + cmd: 'info', + type: 'emote', + text: `@${payload.nick} ${emote}` + }; + + return newPayload; + } + + return payload; +}; + +// module meta +exports.requiredData = ['text']; exports.info = { name: 'chat', - usage: 'chat {text}', - description: 'Broadcasts passed `text` field to the calling users channel' + description: 'Broadcasts passed `text` field to the calling users channel', + usage: ` + API: { cmd: 'chat', text: '' } + Text: Uuuuhm. Just kind type in that little box at the bottom and hit enter.\n + Bonus super secret hidden commands: + /me + /myhash` }; diff --git a/server/src/commands/core/help.js b/server/src/commands/core/help.js index 8fd02a1..60a7280 100644 --- a/server/src/commands/core/help.js +++ b/server/src/commands/core/help.js @@ -2,41 +2,50 @@ Description: Outputs the current command module list or command categories */ +// module support functions const stripIndents = require('common-tags').stripIndents; -exports.run = async (core, server, socket, data) => { - // TODO: this module needs to be clean up badly :( +// module main +exports.run = async (core, server, socket, payload) => { + // check for spam + if (server._police.frisk(socket.remoteAddress, 2)) { + return server.reply({ + cmd: 'warn', + text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.' + }, socket); + } - // verify passed arguments - let typeDt = typeof data.type; - let catDt = typeof data.category; - let cmdDt = typeof data.command; - if (typeDt !== 'undefined' && typeDt !== 'string' ) { - return; - } else if (catDt !== 'undefined' && catDt !== 'string' ) { - return; - } else if (cmdDt !== 'undefined' && cmdDt !== 'string' ) { + // verify user input + if (typeof payload.command !== 'undefined' && typeof payload.command !== 'string') { return; } - // set default reply - let reply = stripIndents`Help usage: - Show all categories -> { cmd: 'help', type: 'categories' } - Show all commands in category -> { cmd: 'help', category: '' } - Show specific command -> { cmd: 'help', command: '' }`; + let reply = ''; + if (typeof payload.command === 'undefined') { + reply = stripIndents`Listing all current commands. For specific help on certain commands, use either: + Text: /help + API: {cmd: 'help', command: ''}`; + reply += '\n\n-------------------------------------\n\n'; - // change reply based on query - if (typeDt !== 'undefined') { let categories = core.commands.categories().sort(); - reply = `Command Categories:\n${categories.map(c => `- ${c.replace('../src/commands/', '')}`).join('\n')}`; - } else if (catDt !== 'undefined') { - let catCommands = core.commands.all('../src/commands/' + data.category).sort((a, b) => a.info.name.localeCompare(b.info.name)); - reply = `${data.category} commands:\n${catCommands.map(c => `- ${c.info.name}`).join('\n')}`; - } else if (cmdDt !== 'undefined') { - let command = core.commands.get(data.command); - reply = stripIndents` - Usage: ${command.info.usage || command.info.name} - Description: ${command.info.description || '¯\_(ツ)_/¯'}`; + for (let i = 0, j = categories.length; i < j; i++) { + reply += `${categories[i].replace('../src/commands/', '').replace(/^\w/, c => c.toUpperCase())} Commands:\n`; + let catCommands = core.commands.all(categories[i]).sort((a, b) => a.info.name.localeCompare(b.info.name)); + reply += ` ${catCommands.map(c => `${c.info.name}`).join(', ')}\n\n`; + } + } else { + let command = core.commands.get(payload.command); + + if (typeof command === 'undefined') { + reply = 'Unknown command'; + } else { + reply = stripIndents`Name: ${command.info.name} + Aliases: ${typeof command.info.aliases !== 'undefined' ? command.info.aliases.join(', ') : 'None'} + Category: ${command.info.category.replace('../src/commands/', '').replace(/^\w/, c => c.toUpperCase())} + Required Parameters: ${command.requiredData || 'None'}\n + Description: ${command.info.description || '¯\_(ツ)_/¯'}\n + Usage: ${command.info.usage || command.info.name}`; + } } // output reply @@ -46,8 +55,36 @@ exports.run = async (core, server, socket, data) => { }, socket); }; +// module hook functions +exports.initHooks = (server) => { + server.registerHook('in', 'chat', this.helpCheck); +}; + +// hooks chat commands checking for /whisper +exports.helpCheck = (core, server, socket, payload) => { + if (typeof payload.text !== 'string') { + return false; + } + + if (payload.text.startsWith('/help')) { + let input = payload.text.substr(1, payload.text.length).split(' ', 2); + + this.run(core, server, socket, { + cmd: input[0], + command: input[1] + }); + + return false; + } + + return payload; +}; + +// module meta exports.info = { name: 'help', - usage: 'help ([ type:categories] | [category: | command: ])', - description: 'Outputs information about the servers current protocol' + description: 'Outputs information about the servers current protocol', + usage: ` + API: { cmd: 'help', command: '' } + Text: /help ` }; diff --git a/server/src/commands/core/invite.js b/server/src/commands/core/invite.js index d616193..f44779b 100644 --- a/server/src/commands/core/invite.js +++ b/server/src/commands/core/invite.js @@ -2,17 +2,17 @@ Description: Generates a semi-unique channel name then broadcasts it to each client */ +// module support functions const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick); +// module main exports.run = async (core, server, socket, data) => { // check for spam if (server._police.frisk(socket.remoteAddress, 2)) { - server.reply({ + return server.reply({ cmd: 'warn', text: 'You are sending invites too fast. Wait a moment before trying again.' }, socket); - - return; } // verify user input @@ -34,16 +34,15 @@ exports.run = async (core, server, socket, data) => { invite: channel, text: `${socket.nick} invited you to ?${channel}` }; + let inviteSent = server.broadcast( payload, { channel: socket.channel, nick: data.nick }); // server indicates the user was not found if (!inviteSent) { - server.reply({ + return server.reply({ cmd: 'warn', text: 'Could not find user in channel' }, socket); - - return; } // reply with common channel @@ -56,10 +55,11 @@ exports.run = async (core, server, socket, data) => { core.managers.stats.increment('invites-sent'); }; +// module meta exports.requiredData = ['nick']; - exports.info = { name: 'invite', - usage: 'invite {nick}', - description: 'Generates a unique (more or less) room name and passes it to two clients' + description: 'Generates a unique (more or less) room name and passes it to two clients', + usage: ` + API: { cmd: 'invite', nick: '' }` }; diff --git a/server/src/commands/core/join.js b/server/src/commands/core/join.js index 31458a2..31bc3c1 100644 --- a/server/src/commands/core/join.js +++ b/server/src/commands/core/join.js @@ -2,6 +2,7 @@ Description: Initial entry point, applies `channel` and `nick` to the calling socket */ +// module support functions const crypto = require('crypto'); const hash = (password) => { @@ -12,15 +13,54 @@ const hash = (password) => { const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick); +// exposed "login" function to allow hooks to verify user join events +// returns object containing user info or string if error +exports.parseNickname = (core, data) => { + let userInfo = { + nick: '', + uType: 'user', + trip: null, + }; + + // seperate nick from password + let nickArray = data.nick.split('#', 2); + userInfo.nick = nickArray[0].trim(); + + if (!verifyNickname(userInfo.nick)) { + // return error as string + return 'Nickname must consist of up to 24 letters, numbers, and underscores'; + } + + let password = nickArray[1]; + if (userInfo.nick.toLowerCase() == core.config.adminName.toLowerCase()) { + if (password !== core.config.adminPass) { + return 'You are not the admin, liar!'; + } else { + userInfo.uType = 'admin'; + userInfo.trip = 'Admin'; + } + } else if (password) { + userInfo.trip = hash(password + core.config.tripSalt); + } + + // TODO: disallow moderator impersonation + for (let mod of core.config.mods) { + if (userInfo.trip === mod.trip) { + userInfo.uType = 'mod'; + } + } + + return userInfo; +}; + +// module main exports.run = async (core, server, socket, data) => { // check for spam if (server._police.frisk(socket.remoteAddress, 3)) { - server.reply({ + return server.reply({ cmd: 'warn', text: 'You are joining channels too fast. Wait a moment and try again.' }, socket); - - return; } // calling socket already in a channel @@ -39,75 +79,39 @@ exports.run = async (core, server, socket, data) => { return; } - // process nickname - let nick = data.nick; - let nickArray = nick.split('#', 2); - nick = nickArray[0].trim(); - - if (!verifyNickname(nick)) { - server.reply({ + let userInfo = this.parseNickname(core, data); + if (typeof userInfo === 'string') { + return server.reply({ cmd: 'warn', - text: 'Nickname must consist of up to 24 letters, numbers, and underscores' + text: userInfo }, socket); - - return; } // check if the nickname already exists in the channel let userExists = server.findSockets({ channel: data.channel, - nick: (targetNick) => targetNick.toLowerCase() === nick.toLowerCase() + nick: (targetNick) => targetNick.toLowerCase() === userInfo.nick.toLowerCase() }); if (userExists.length > 0) { // that nickname is already in that channel - server.reply({ + return server.reply({ cmd: 'warn', text: 'Nickname taken' }, socket); - - return; } - // TODO: should we check for mod status first to prevent overwriting of admin status somehow? Meh, w/e, cba. - let uType = 'user'; - let trip = null; - let password = nickArray[1]; - if (nick.toLowerCase() == core.config.adminName.toLowerCase()) { - if (password != core.config.adminPass) { - server._police.frisk(socket.remoteAddress, 4); - - server.reply({ - cmd: 'warn', - text: 'Gtfo' - }, socket); - - return; - } else { - uType = 'admin'; - trip = 'Admin'; - } - } else if (password) { - trip = hash(password + core.config.tripSalt); - } - - // TODO: disallow moderator impersonation - for (let mod of core.config.mods) { - if (trip === mod.trip) { - uType = 'mod'; - } - } + userInfo.userHash = server.getSocketHash(socket); // prepare to notify channel peers let newPeerList = server.findSockets({ channel: data.channel }); - let userHash = server.getSocketHash(socket); let nicks = []; let joinAnnouncement = { cmd: 'onlineAdd', - nick: nick, - trip: trip || 'null', - hash: userHash + nick: userInfo.nick, + trip: userInfo.trip || 'null', + hash: userInfo.userHash }; // send join announcement and prep online set @@ -117,11 +121,11 @@ exports.run = async (core, server, socket, data) => { } // store user info - socket.uType = uType; - socket.nick = nick; - socket.channel = channel; - socket.hash = userHash; - if (trip !== null) socket.trip = trip; + socket.uType = userInfo.uType; + socket.nick = userInfo.nick; + socket.channel = data.channel; + socket.hash = userInfo.userHash; + if (userInfo.trip !== null) socket.trip = userInfo.trip; nicks.push(socket.nick); @@ -135,10 +139,11 @@ exports.run = async (core, server, socket, data) => { core.managers.stats.increment('users-joined'); }; +// module meta exports.requiredData = ['channel', 'nick']; - exports.info = { name: 'join', - usage: 'join {channel} {nick}', - description: 'Place calling socket into target channel with target nick & broadcast event to channel' + description: 'Place calling socket into target channel with target nick & broadcast event to channel', + usage: ` + API: { cmd: 'join', nick: '', channel: '' }` }; diff --git a/server/src/commands/core/morestats.js b/server/src/commands/core/morestats.js index b64d478..e8eed05 100644 --- a/server/src/commands/core/morestats.js +++ b/server/src/commands/core/morestats.js @@ -2,6 +2,7 @@ Description: Outputs more info than the legacy stats command */ +// module support functions const stripIndents = require('common-tags').stripIndents; const formatTime = (time) => { @@ -19,6 +20,7 @@ const formatTime = (time) => { return `${days.toFixed(0)}d ${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`; }; +// module main exports.run = async (core, server, socket, data) => { // gather connection and channel count let ips = {}; @@ -54,7 +56,33 @@ exports.run = async (core, server, socket, data) => { core.managers.stats.increment('stats-requested'); }; +// module hook functions +exports.initHooks = (server) => { + server.registerHook('in', 'chat', this.statsCheck); +}; + +// hooks chat commands checking for /stats +exports.statsCheck = (core, server, socket, payload) => { + if (typeof payload.text !== 'string') { + return false; + } + + if (payload.text.startsWith('/stats')) { + this.run(core, server, socket, { + cmd: 'morestats' + }); + + return false; + } + + return payload; +}; + +// module meta exports.info = { name: 'morestats', - description: 'Sends back current server stats to the calling client' + description: 'Sends back current server stats to the calling client', + usage: ` + API: { cmd: 'morestats' } + Text: /stats` }; diff --git a/server/src/commands/core/move.js b/server/src/commands/core/move.js index 284a38d..e85f481 100644 --- a/server/src/commands/core/move.js +++ b/server/src/commands/core/move.js @@ -2,15 +2,14 @@ Description: Changes the current channel of the calling socket */ +// module main exports.run = async (core, server, socket, data) => { // check for spam if (server._police.frisk(socket.remoteAddress, 6)) { - server.reply({ + return server.reply({ cmd: 'warn', text: 'You are changing channels too fast. Wait a moment before trying again.' }, socket); - - return; } // check user input @@ -81,10 +80,11 @@ exports.run = async (core, server, socket, data) => { socket.channel = data.channel; }; +// module meta exports.requiredData = ['channel']; - exports.info = { name: 'move', - usage: 'move {channel}', - description: 'This will change the current channel to the new one provided' + description: 'This will change your current channel to the new one provided', + usage: ` + API: { cmd: 'move', channel: '' }` }; diff --git a/server/src/commands/core/ping.js b/server/src/commands/core/ping.js index cf1d8a4..1e710e5 100644 --- a/server/src/commands/core/ping.js +++ b/server/src/commands/core/ping.js @@ -2,10 +2,12 @@ Description: This module is only in place to supress error notices legacy sources may get */ +// module main exports.run = async (core, server, socket, data) => { return; }; +// module meta exports.info = { name: 'ping', description: 'This module is only in place to supress error notices legacy sources may get' diff --git a/server/src/commands/core/stats.js b/server/src/commands/core/stats.js index 74bd4a5..b271bb1 100644 --- a/server/src/commands/core/stats.js +++ b/server/src/commands/core/stats.js @@ -2,6 +2,7 @@ Description: Legacy stats output, kept for compatibility, outputs user and channel count */ +// module main exports.run = async (core, server, socket, data) => { // gather connection and channel count let ips = {}; @@ -29,7 +30,10 @@ exports.run = async (core, server, socket, data) => { core.managers.stats.increment('stats-requested'); }; +// module meta exports.info = { name: 'stats', - description: 'Sends back legacy server stats to the calling client' + description: 'Sends back legacy server stats to the calling client', + usage: ` + API: { cmd: 'stats' }` }; diff --git a/server/src/commands/core/whisper.js b/server/src/commands/core/whisper.js new file mode 100644 index 0000000..2ad2df0 --- /dev/null +++ b/server/src/commands/core/whisper.js @@ -0,0 +1,110 @@ +/* + Description: Display text on targets screen that only they can see +*/ + +// module support functions +const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick); + +const parseText = (text) => { + // verifies user input is text + if (typeof text !== 'string') { + return false; + } + + // strip newlines from beginning and end + text = text.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); + // replace 3+ newlines with just 2 newlines + text = text.replace(/\n{3,}/g, "\n\n"); + + return text; +}; + +// module main +exports.run = async (core, server, socket, payload) => { + // check user input + let text = parseText(payload.text); + + if (!text) { + // lets not send objects or empty text, yea? + return server._police.frisk(socket.remoteAddress, 13); + } + + // check for spam + let score = text.length / 83 / 4; + if (server._police.frisk(socket.remoteAddress, score)) { + return server.reply({ + cmd: 'warn', + text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.' + }, socket); + } + + let targetNick = payload.nick; + if (!verifyNickname(targetNick)) { + return; + } + + // find target user + let targetClient = server.findSockets({ channel: socket.channel, nick: targetNick }); + + if (targetClient.length === 0) { + return server.reply({ + cmd: 'warn', + text: 'Could not find user in channel' + }, socket); + } + + targetClient = targetClient[0]; + + server.reply({ + cmd: 'info', + type: 'whisper', + from: socket.nick, + trip: socket.trip || 'null', + text: `${socket.nick} whispered: ${text}` + }, targetClient); + + server.reply({ + cmd: 'info', + type: 'whisper', + text: `You whispered to @${targetNick}: ${text}` + }, socket); +}; + +// module hook functions +exports.initHooks = (server) => { + server.registerHook('in', 'chat', this.whisperCheck); +}; + +// hooks chat commands checking for /whisper +exports.whisperCheck = (core, server, socket, payload) => { + if (typeof payload.text !== 'string') { + return false; + } + + if (payload.text.startsWith('/whisper')) { + let input = payload.text.split(' '); + let target = input[1].replace(/@/g, ''); + input.splice(0, 2); + let whisperText = input.join(' '); + + this.run(core, server, socket, { + cmd: 'whisper', + nick: target, + text: whisperText + }); + + return false; + } + + return payload; +}; + +// module meta +exports.requiredData = ['nick', 'text']; +exports.info = { + name: 'whisper', + description: 'Display text on targets screen that only they can see', + usage: ` + API: { cmd: 'whisper', nick: '', text: '' } + Text: /whisper ` +}; -- cgit v1.2.1