aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/commands/core
diff options
context:
space:
mode:
authormarzavec <admin@marzavec.com>2018-09-30 08:44:36 +0200
committermarzavec <admin@marzavec.com>2018-09-30 08:44:36 +0200
commitc719020e17cb1c98da55be6cc7efe0e50ab51ffa (patch)
tree4c1e7f05aec2b6a995e21d2bbecbb45c2ae14bd6 /server/src/commands/core
parentMerge pull request #28 from henrywright/27 (diff)
downloadhackchat-c719020e17cb1c98da55be6cc7efe0e50ab51ffa.tar.gz
hackchat-c719020e17cb1c98da55be6cc7efe0e50ab51ffa.zip
Added hooks, modules and cleaned up code
Diffstat (limited to 'server/src/commands/core')
-rw-r--r--server/src/commands/core/changenick.js30
-rw-r--r--server/src/commands/core/chat.js90
-rw-r--r--server/src/commands/core/help.js95
-rw-r--r--server/src/commands/core/invite.js18
-rw-r--r--server/src/commands/core/join.js119
-rw-r--r--server/src/commands/core/morestats.js30
-rw-r--r--server/src/commands/core/move.js12
-rw-r--r--server/src/commands/core/ping.js2
-rw-r--r--server/src/commands/core/stats.js6
-rw-r--r--server/src/commands/core/whisper.js110
10 files changed, 372 insertions, 140 deletions
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: '<new nickname>' }`
};
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 to send>' }
+ Text: Uuuuhm. Just kind type in that little box at the bottom and hit enter.\n
+ Bonus super secret hidden commands:
+ /me <emote>
+ /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: '<category name>' }
- Show specific command -> { cmd: 'help', command: '<command name>' }`;
+ let reply = '';
+ if (typeof payload.command === 'undefined') {
+ reply = stripIndents`Listing all current commands. For specific help on certain commands, use either:
+ Text: /help <command name>
+ API: {cmd: 'help', command: '<command name>'}`;
+ 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:<category name> | command:<command name> ])',
- description: 'Outputs information about the servers current protocol'
+ description: 'Outputs information about the servers current protocol',
+ usage: `
+ API: { cmd: 'help', command: '<optional command name>' }
+ Text: /help <optional command name>`
};
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: '<target nickname>' }`
};
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: '<your nickname>', channel: '<target 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: '<target 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: '<target name>', text: '<text to whisper>' }
+ Text: /whisper <target name> <text to whisper>`
+};