1
0
mirror of https://github.com/hack-chat/main.git synced 2024-03-22 13:20:33 +08:00

Made all indentation tabs

This commit is contained in:
MinusGix 2018-03-10 00:49:55 -06:00
parent 584813fb23
commit 56106dfa2f
25 changed files with 1262 additions and 1262 deletions

View File

@ -1,11 +1,11 @@
/** /**
* HackChat main server entry point * HackChat main server entry point
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';

View File

@ -5,43 +5,43 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (socket.uType != 'admin') { if (socket.uType != 'admin') {
// ignore if not admin // ignore if not admin
return; return;
} }
let mod = { let mod = {
trip: data.trip trip: data.trip
} }
core.config.mods.push(mod); // purposely not using `config.set()` to avoid auto-save core.config.mods.push(mod); // purposely not using `config.set()` to avoid auto-save
for (let client of server.clients) { for (let client of server.clients) {
if (typeof client.trip !== 'undefined' && client.trip === data.trip) { if (typeof client.trip !== 'undefined' && client.trip === data.trip) {
client.uType = 'mod'; client.uType = 'mod';
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: 'You are now a mod.' text: 'You are now a mod.'
}, client); }, client);
} }
} }
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: `Added mod trip: ${data.trip}` text: `Added mod trip: ${data.trip}`
}, socket); }, socket);
server.broadcast({ server.broadcast({
cmd: 'info', cmd: 'info',
text: `Added mod trip: ${data.trip}` text: `Added mod trip: ${data.trip}`
}, { uType: 'mod' }); }, { uType: 'mod' });
}; };
exports.requiredData = ['trip']; exports.requiredData = ['trip'];
exports.info = { exports.info = {
name: 'addmod', name: 'addmod',
usage: 'addmod {trip}', usage: 'addmod {trip}',
description: 'Adds target trip to the config as a mod and upgrades the socket type' description: 'Adds target trip to the config as a mod and upgrades the socket type'
}; };

View File

@ -5,37 +5,37 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (socket.uType != 'admin') { if (socket.uType != 'admin') {
// ignore if not admin // ignore if not admin
return; return;
} }
let channels = {}; let channels = {};
for (var client of server.clients) { for (var client of server.clients) {
if (client.channel) { if (client.channel) {
if (!channels[client.channel]) { if (!channels[client.channel]) {
channels[client.channel] = []; channels[client.channel] = [];
} }
channels[client.channel].push(client.nick); channels[client.channel].push(client.nick);
} }
} }
let lines = []; let lines = [];
for (let channel in channels) { for (let channel in channels) {
lines.push(`?${channel} ${channels[channel].join(", ")}`); lines.push(`?${channel} ${channels[channel].join(", ")}`);
} }
let text = ''; let text = '';
text += lines.join("\n"); text += lines.join("\n");
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: text text: text
}, socket); }, socket);
}; };
exports.info = { exports.info = {
name: 'listusers', name: 'listusers',
usage: 'listusers', usage: 'listusers',
description: 'Outputs all current channels and sockets in those channels' description: 'Outputs all current channels and sockets in those channels'
}; };

View File

@ -5,30 +5,30 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (socket.uType != 'admin') { if (socket.uType != 'admin') {
// ignore if not admin // ignore if not admin
return; return;
} }
let loadResult = core.managers.dynamicImports.reloadDirCache('src/commands'); let loadResult = core.managers.dynamicImports.reloadDirCache('src/commands');
loadResult += core.commands.loadCommands(); loadResult += core.commands.loadCommands();
if (loadResult == '') if (loadResult == '')
loadResult = 'Commands reloaded without errors!'; loadResult = 'Commands reloaded without errors!';
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: loadResult text: loadResult
}, socket); }, socket);
server.broadcast({ server.broadcast({
cmd: 'info', cmd: 'info',
text: loadResult text: loadResult
}, { uType: 'mod' }); }, { uType: 'mod' });
}; };
exports.info = { exports.info = {
name: 'reload', name: 'reload',
usage: 'reload', usage: 'reload',
description: '(Re)loads any new commands into memory, outputs errors if any' description: '(Re)loads any new commands into memory, outputs errors if any'
}; };

View File

@ -5,30 +5,30 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (socket.uType != 'admin') { if (socket.uType != 'admin') {
// ignore if not admin // ignore if not admin
return; return;
} }
let saveResult = core.managers.config.save(); let saveResult = core.managers.config.save();
if (!saveResult) { if (!saveResult) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Failed to save config, check logs.' text: 'Failed to save config, check logs.'
}, client); }, client);
return; return;
} }
server.broadcast({ server.broadcast({
cmd: 'info', cmd: 'info',
text: 'Config saved!' text: 'Config saved!'
}, { uType: 'mod' }); }, { uType: 'mod' });
}; };
exports.info = { exports.info = {
name: 'saveconfig', name: 'saveconfig',
usage: 'saveconfig', usage: 'saveconfig',
description: 'Saves current config' description: 'Saves current config'
}; };

View File

@ -5,21 +5,21 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (socket.uType != 'admin') { if (socket.uType != 'admin') {
// ignore if not admin // ignore if not admin
return; return;
} }
server.broadcast( { server.broadcast( {
cmd: 'info', cmd: 'info',
text: `Server Notice: ${data.text}` text: `Server Notice: ${data.text}`
}, {}); }, {});
}; };
exports.requiredData = ['text']; exports.requiredData = ['text'];
exports.info = { exports.info = {
name: 'shout', name: 'shout',
usage: 'shout {text}', usage: 'shout {text}',
description: 'Displays passed text to every client connected' description: 'Displays passed text to every client connected'
}; };

View File

@ -5,52 +5,52 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
// process text // process text
let text = String(data.text); let text = String(data.text);
// strip newlines from beginning and end // strip newlines from beginning and end
text = text.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); text = text.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
// replace 3+ newlines with just 2 newlines // replace 3+ newlines with just 2 newlines
text = text.replace(/\n{3,}/g, "\n\n"); text = text.replace(/\n{3,}/g, "\n\n");
if (!text) { if (!text) {
// lets not send empty text? // lets not send empty text?
return; return;
} }
let score = text.length / 83 / 4; let score = text.length / 83 / 4;
if (server._police.frisk(socket.remoteAddress, score)) { if (server._police.frisk(socket.remoteAddress, score)) {
server.reply({ server.reply({
cmd: 'warn', 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.' text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.'
}, socket); }, socket);
return; return;
} }
let payload = { let payload = {
cmd: 'chat', cmd: 'chat',
nick: socket.nick, nick: socket.nick,
text: text text: text
}; };
if (socket.uType == 'admin') { if (socket.uType == 'admin') {
payload.admin = true; payload.admin = true;
} else if (socket.uType == 'mod') { } else if (socket.uType == 'mod') {
payload.mod = true; payload.mod = true;
} }
if (socket.trip) { if (socket.trip) {
payload.trip = socket.trip; payload.trip = socket.trip;
} }
server.broadcast( payload, { channel: socket.channel }); server.broadcast( payload, { channel: socket.channel });
core.managers.stats.increment('messages-sent'); core.managers.stats.increment('messages-sent');
}; };
exports.requiredData = ['text']; exports.requiredData = ['text'];
exports.info = { exports.info = {
name: 'chat', name: 'chat',
usage: 'chat {text}', usage: 'chat {text}',
description: 'Broadcasts passed `text` field to the calling users channel' description: 'Broadcasts passed `text` field to the calling users channel'
}; };

View File

@ -5,29 +5,29 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
let reply = `Help usage: { cmd: 'help', type: 'categories'} or { cmd: 'help', type: 'commandname'}`; let reply = `Help usage: { cmd: 'help', type: 'categories'} or { cmd: 'help', type: 'commandname'}`;
if (typeof data.type === 'undefined') { if (typeof data.type === 'undefined') {
// //
} else { } else {
if (data.type == 'categories') { if (data.type == 'categories') {
let categories = core.commands.categories(); let categories = core.commands.categories();
// TODO: bad output, fix this // TODO: bad output, fix this
reply = `Command Categories:\n${categories}`; reply = `Command Categories:\n${categories}`;
} else { } else {
// TODO: finish this module later // TODO: finish this module later
} }
} }
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: reply text: reply
}, socket); }, socket);
}; };
// optional parameters are marked, all others are required // optional parameters are marked, all others are required
exports.info = { exports.info = {
name: 'help', // actual command name name: 'help', // actual command name
usage: 'help ([type:categories] | [type:command])', usage: 'help ([type:categories] | [type:command])',
description: 'Outputs information about the servers current protocol' description: 'Outputs information about the servers current protocol'
}; };

View File

@ -5,60 +5,60 @@
'use strict'; 'use strict';
function verifyNickname(nick) { function verifyNickname(nick) {
return /^[a-zA-Z0-9_]{1,24}$/.test(nick); return /^[a-zA-Z0-9_]{1,24}$/.test(nick);
} }
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
let targetNick = String(data.nick); let targetNick = String(data.nick);
if (!verifyNickname(targetNick)) { if (!verifyNickname(targetNick)) {
// Not a valid nickname? Chances are we won't find them // Not a valid nickname? Chances are we won't find them
return; return;
} }
if (targetNick == socket.nick) { if (targetNick == socket.nick) {
// TODO: reply with something witty? They invited themself // TODO: reply with something witty? They invited themself
return; return;
} }
if (server._police.frisk(socket.remoteAddress, 2)) { if (server._police.frisk(socket.remoteAddress, 2)) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'You are sending invites too fast. Wait a moment before trying again.' text: 'You are sending invites too fast. Wait a moment before trying again.'
}, socket); }, socket);
return; return;
} }
let channel = Math.random().toString(36).substr(2, 8); let channel = Math.random().toString(36).substr(2, 8);
let payload = { let payload = {
cmd: 'info', cmd: 'info',
text: `${socket.nick} invited you to ?${channel}` text: `${socket.nick} invited you to ?${channel}`
}; };
let inviteSent = server.broadcast( payload, { channel: socket.channel, nick: targetNick }); let inviteSent = server.broadcast( payload, { channel: socket.channel, nick: targetNick });
if (!inviteSent) { if (!inviteSent) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Could not find user in channel' text: 'Could not find user in channel'
}, socket); }, socket);
return; return;
} }
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: `You invited ${targetNick} to ?${channel}` text: `You invited ${targetNick} to ?${channel}`
}, socket); }, socket);
core.managers.stats.increment('invites-sent'); core.managers.stats.increment('invites-sent');
}; };
exports.requiredData = ['nick']; exports.requiredData = ['nick'];
exports.info = { exports.info = {
name: 'invite', name: 'invite',
usage: 'invite {nick}', 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'
}; };

View File

@ -7,122 +7,122 @@
const crypto = require('crypto'); const crypto = require('crypto');
function hash(password) { function hash(password) {
var sha = crypto.createHash('sha256'); var sha = crypto.createHash('sha256');
sha.update(password); sha.update(password);
return sha.digest('base64').substr(0, 6); return sha.digest('base64').substr(0, 6);
} }
function verifyNickname(nick) { function verifyNickname(nick) {
return /^[a-zA-Z0-9_]{1,24}$/.test(nick); return /^[a-zA-Z0-9_]{1,24}$/.test(nick);
} }
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (server._police.frisk(socket.remoteAddress, 3)) { if (server._police.frisk(socket.remoteAddress, 3)) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'You are joining channels too fast. Wait a moment and try again.' text: 'You are joining channels too fast. Wait a moment and try again.'
}, socket); }, socket);
return; return;
} }
if (typeof socket.channel !== 'undefined') { if (typeof socket.channel !== 'undefined') {
// Calling socket already in a channel // Calling socket already in a channel
// TODO: allow changing of channel without reconnection // TODO: allow changing of channel without reconnection
return; return;
} }
let channel = String(data.channel).trim(); let channel = String(data.channel).trim();
if (!channel) { if (!channel) {
// Must join a non-blank channel // Must join a non-blank channel
return; return;
} }
// Process nickname // Process nickname
let nick = String(data.nick); let nick = String(data.nick);
let nickArray = nick.split('#', 2); let nickArray = nick.split('#', 2);
nick = nickArray[0].trim(); nick = nickArray[0].trim();
if (!verifyNickname(nick)) { if (!verifyNickname(nick)) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores' text: 'Nickname must consist of up to 24 letters, numbers, and underscores'
}, socket); }, socket);
return return
} }
for (let client of server.clients) { for (let client of server.clients) {
if (client.channel === channel) { if (client.channel === channel) {
if (client.nick.toLowerCase() === nick.toLowerCase()) { if (client.nick.toLowerCase() === nick.toLowerCase()) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Nickname taken' text: 'Nickname taken'
}, socket); }, socket);
return; return;
} }
} }
} }
// TODO: Should we check for mod status first to prevent overwriting of admin status somehow? Meh, w/e, cba. // TODO: Should we check for mod status first to prevent overwriting of admin status somehow? Meh, w/e, cba.
let uType = 'user'; let uType = 'user';
let trip = null; let trip = null;
let password = nickArray[1]; let password = nickArray[1];
if (nick.toLowerCase() == core.config.adminName.toLowerCase()) { if (nick.toLowerCase() == core.config.adminName.toLowerCase()) {
if (password != core.config.adminPass) { if (password != core.config.adminPass) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Gtfo' text: 'Gtfo'
}, socket); }, socket);
return; return;
} else { } else {
uType = 'admin'; uType = 'admin';
trip = hash(password + core.config.tripSalt); trip = hash(password + core.config.tripSalt);
} }
} else if (password) { } else if (password) {
trip = hash(password + core.config.tripSalt); trip = hash(password + core.config.tripSalt);
} }
// TODO: Disallow moderator impersonation // TODO: Disallow moderator impersonation
for (let mod of core.config.mods) { for (let mod of core.config.mods) {
if (trip === mod.trip) if (trip === mod.trip)
uType = 'mod'; uType = 'mod';
} }
// Announce the new user // Announce the new user
server.broadcast({ server.broadcast({
cmd: 'onlineAdd', cmd: 'onlineAdd',
nick: nick, nick: nick,
trip: trip || 'null' trip: trip || 'null'
}, { channel: channel }); }, { channel: channel });
socket.uType = uType; socket.uType = uType;
socket.nick = nick; socket.nick = nick;
socket.channel = channel; socket.channel = channel;
if (trip !== null) socket.trip = trip; if (trip !== null) socket.trip = trip;
// Reply with online user list // Reply with online user list
let nicks = []; let nicks = [];
for (let client of server.clients) { for (let client of server.clients) {
if (client.channel === channel) { if (client.channel === channel) {
nicks.push(client.nick); nicks.push(client.nick);
} }
} }
server.reply({ server.reply({
cmd: 'onlineSet', cmd: 'onlineSet',
nicks: nicks nicks: nicks
}, socket); }, socket);
core.managers.stats.increment('users-joined'); core.managers.stats.increment('users-joined');
}; };
exports.requiredData = ['channel', 'nick']; exports.requiredData = ['channel', 'nick'];
exports.info = { exports.info = {
name: 'join', name: 'join',
usage: 'join {channel} {nick}', 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'
}; };

View File

@ -8,20 +8,20 @@
// this function will only be only in the scope of the module // this function will only be only in the scope of the module
const createReply = (echoInput) => { const createReply = (echoInput) => {
if (echoInput.length > 100) if (echoInput.length > 100)
echoInput = 'HOW ABOUT NO?'; echoInput = 'HOW ABOUT NO?';
return `You want me to echo: ${echoInput}?` return `You want me to echo: ${echoInput}?`
}; };
// `exports.run()` is required and will always be passed (core, server, socket, data) // `exports.run()` is required and will always be passed (core, server, socket, data)
// be sure it's asyn too // be sure it's asyn too
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: `SHOWCASE MODULE: ${core.showcase} - ${this.createReply(data.echo)}` text: `SHOWCASE MODULE: ${core.showcase} - ${this.createReply(data.echo)}`
}, socket); }, socket);
}; };
@ -29,9 +29,9 @@ exports.run = async (core, server, socket, data) => {
// it will always be passed a reference to the global core class // it will always be passed a reference to the global core class
// note: this will fire again if a reload is issued, keep that in mind // note: this will fire again if a reload is issued, keep that in mind
exports.init = (core) => { exports.init = (core) => {
if (typeof core.showcase === 'undefined') { if (typeof core.showcase === 'undefined') {
core.showcase = 'init is a handy place to put global data by assigning it to `core`'; core.showcase = 'init is a handy place to put global data by assigning it to `core`';
} }
} }
// optional, if `data.echo` is missing `exports.run()` will never be called & the user will be alerted // optional, if `data.echo` is missing `exports.run()` will never be called & the user will be alerted
@ -39,8 +39,8 @@ exports.requiredData = ['echo'];
// optional parameters are marked, all others are required // optional parameters are marked, all others are required
exports.info = { exports.info = {
name: 'showcase', // actual command name name: 'showcase', // actual command name
aliases: ['templateModule'], // optional, an array of other names this module can be executed by aliases: ['templateModule'], // optional, an array of other names this module can be executed by
usage: 'showcase {echo}', // used for help output usage: 'showcase {echo}', // used for help output
description: 'Simple command module template & info' // used for help output description: 'Simple command module template & info' // used for help output
}; };

View File

@ -7,49 +7,49 @@
const stripIndents = require('common-tags').stripIndents; const stripIndents = require('common-tags').stripIndents;
const formatTime = (time) => { const formatTime = (time) => {
let seconds = time[0] + time[1] / 1e9; let seconds = time[0] + time[1] / 1e9;
let minutes = Math.floor(seconds / 60); let minutes = Math.floor(seconds / 60);
seconds = seconds % 60; seconds = seconds % 60;
let hours = Math.floor(minutes / 60); let hours = Math.floor(minutes / 60);
minutes = minutes % 60; minutes = minutes % 60;
return `${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`; return `${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`;
}; };
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
let ips = {}; let ips = {};
let channels = {}; let channels = {};
for (let client of server.clients) { for (let client of server.clients) {
if (client.channel) { if (client.channel) {
channels[client.channel] = true; channels[client.channel] = true;
ips[client.remoteAddress] = true; ips[client.remoteAddress] = true;
} }
} }
let uniqueClientCount = Object.keys(ips).length; let uniqueClientCount = Object.keys(ips).length;
let uniqueChannels = Object.keys(channels).length; let uniqueChannels = Object.keys(channels).length;
ips = null; ips = null;
channels = null; channels = null;
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: stripIndents`current-connections: ${uniqueClientCount} text: stripIndents`current-connections: ${uniqueClientCount}
current-channels: ${uniqueChannels} current-channels: ${uniqueChannels}
users-joined: ${(core.managers.stats.get('users-joined') || 0)} users-joined: ${(core.managers.stats.get('users-joined') || 0)}
invites-sent: ${(core.managers.stats.get('invites-sent') || 0)} invites-sent: ${(core.managers.stats.get('invites-sent') || 0)}
messages-sent: ${(core.managers.stats.get('messages-sent') || 0)} messages-sent: ${(core.managers.stats.get('messages-sent') || 0)}
users-banned: ${(core.managers.stats.get('users-banned') || 0)} users-banned: ${(core.managers.stats.get('users-banned') || 0)}
stats-requested: ${(core.managers.stats.get('stats-requested') || 0)} stats-requested: ${(core.managers.stats.get('stats-requested') || 0)}
server-uptime: ${formatTime(process.hrtime(core.managers.stats.get('start-time')))}` server-uptime: ${formatTime(process.hrtime(core.managers.stats.get('start-time')))}`
}, socket); }, socket);
core.managers.stats.increment('stats-requested'); core.managers.stats.increment('stats-requested');
}; };
exports.info = { exports.info = {
name: 'stats', name: 'stats',
usage: 'stats', usage: 'stats',
description: 'Sends back current server stats to the calling client' description: 'Sends back current server stats to the calling client'
}; };

View File

@ -5,57 +5,57 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (socket.uType == 'user') { if (socket.uType == 'user') {
// ignore if not mod or admin // ignore if not mod or admin
return; return;
} }
let targetNick = String(data.nick); let targetNick = String(data.nick);
let badClient = null; let badClient = null;
for (let client of server.clients) { for (let client of server.clients) {
// Find badClient's socket // Find badClient's socket
if (client.channel == socket.channel && client.nick == targetNick) { if (client.channel == socket.channel && client.nick == targetNick) {
badClient = client; badClient = client;
break; break;
} }
} }
if (!badClient) { if (!badClient) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Could not find user in channel' text: 'Could not find user in channel'
}, socket); }, socket);
return; return;
} }
if (badClient.uType !== 'user') { if (badClient.uType !== 'user') {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Cannot ban other mods, how rude' text: 'Cannot ban other mods, how rude'
}, socket); }, socket);
return; return;
} }
// TODO: ratelimiting here // TODO: ratelimiting here
// TODO: add reference to banned users nick or unban by nick cmd // TODO: add reference to banned users nick or unban by nick cmd
//POLICE.arrest(getAddress(badClient)) //POLICE.arrest(getAddress(badClient))
// TODO: add event to log? // TODO: add event to log?
console.log(`${socket.nick} [${socket.trip}] banned ${targetNick} in ${socket.channel}`); console.log(`${socket.nick} [${socket.trip}] banned ${targetNick} in ${socket.channel}`);
server.broadcast({ server.broadcast({
cmd: 'info', cmd: 'info',
text: `Banned ${targetNick}` text: `Banned ${targetNick}`
}, { channel: socket.channel }); }, { channel: socket.channel });
badClient.close(); badClient.close();
core.managers.stats.increment('users-banned'); core.managers.stats.increment('users-banned');
}; };
exports.requiredData = ['nick']; exports.requiredData = ['nick'];
exports.info = { exports.info = {
name: 'ban', name: 'ban',
usage: 'ban {nick}', usage: 'ban {nick}',
description: 'Disconnects the target nickname in the same channel as calling socket & adds to ratelimiter' description: 'Disconnects the target nickname in the same channel as calling socket & adds to ratelimiter'
}; };

View File

@ -5,70 +5,70 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (socket.uType == 'user') { if (socket.uType == 'user') {
// ignore if not mod or admin // ignore if not mod or admin
return; return;
} }
let targetNick = String(data.nick); let targetNick = String(data.nick);
let badClient = null; let badClient = null;
for (let client of server.clients) { for (let client of server.clients) {
// Find badClient's socket // Find badClient's socket
if (client.channel == socket.channel && client.nick == targetNick) { if (client.channel == socket.channel && client.nick == targetNick) {
badClient = client; badClient = client;
break; break;
} }
} }
if (!badClient) { if (!badClient) {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Could not find user in channel' text: 'Could not find user in channel'
}, socket); }, socket);
return; return;
} }
if (badClient.uType !== 'user') { if (badClient.uType !== 'user') {
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: 'Cannot kick other mods, how rude' text: 'Cannot kick other mods, how rude'
}, socket); }, socket);
return; return;
} }
// TODO: add event to log? // TODO: add event to log?
let newChannel = Math.random().toString(36).substr(2, 8); let newChannel = Math.random().toString(36).substr(2, 8);
badClient.channel = newChannel; badClient.channel = newChannel;
console.log(`${socket.nick} [${socket.trip}] kicked ${targetNick} in ${socket.channel}`); console.log(`${socket.nick} [${socket.trip}] kicked ${targetNick} in ${socket.channel}`);
// remove socket from same-channel client // remove socket from same-channel client
server.broadcast({ server.broadcast({
cmd: 'onlineRemove', cmd: 'onlineRemove',
nick: targetNick nick: targetNick
}, { channel: socket.channel }); }, { channel: socket.channel });
// publicly broadcast event (TODO: should this be supressed?) // publicly broadcast event (TODO: should this be supressed?)
server.broadcast({ server.broadcast({
cmd: 'info', cmd: 'info',
text: `Kicked ${targetNick}` text: `Kicked ${targetNick}`
}, { channel: socket.channel }); }, { channel: socket.channel });
// inform mods with where they were sent // inform mods with where they were sent
server.broadcast({ server.broadcast({
cmd: 'info', cmd: 'info',
text: `${targetNick} was banished to ?${newChannel}` text: `${targetNick} was banished to ?${newChannel}`
}, { channel: socket.channel, uType: 'mod' }); }, { channel: socket.channel, uType: 'mod' });
core.managers.stats.increment('users-banned'); core.managers.stats.increment('users-banned');
}; };
exports.requiredData = ['nick']; exports.requiredData = ['nick'];
exports.info = { exports.info = {
name: 'kick', name: 'kick',
usage: 'kick {nick}', usage: 'kick {nick}',
description: 'Forces target client into another channel without announcing change' description: 'Forces target client into another channel without announcing change'
}; };

View File

@ -5,30 +5,30 @@
'use strict'; 'use strict';
exports.run = async (core, server, socket, data) => { exports.run = async (core, server, socket, data) => {
if (socket.uType == 'user') { if (socket.uType == 'user') {
// ignore if not mod or admin // ignore if not mod or admin
return; return;
} }
let ip = String(data.ip); let ip = String(data.ip);
let nick = String(data.nick); // for future upgrade let nick = String(data.nick); // for future upgrade
// TODO: remove ip from ratelimiter // TODO: remove ip from ratelimiter
// POLICE.pardon(ip) // POLICE.pardon(ip)
console.log(`${socket.nick} [${socket.trip}] unbanned ${/*nick || */ip} in ${socket.channel}`); console.log(`${socket.nick} [${socket.trip}] unbanned ${/*nick || */ip} in ${socket.channel}`);
server.reply({ server.reply({
cmd: 'info', cmd: 'info',
text: `Unbanned ${/*nick || */ip}` text: `Unbanned ${/*nick || */ip}`
}, socket); }, socket);
core.managers.stats.decrement('users-banned'); core.managers.stats.decrement('users-banned');
}; };
exports.requiredData = ['ip']; exports.requiredData = ['ip'];
exports.info = { exports.info = {
name: 'unban', name: 'unban',
usage: 'unban {ip}', usage: 'unban {ip}',
description: 'Removes target ip from the ratelimiter' description: 'Removes target ip from the ratelimiter'
}; };

View File

@ -1,104 +1,104 @@
/** /**
* Tracks frequency of occurances based on `id` (remote address), then allows or * Tracks frequency of occurances based on `id` (remote address), then allows or
* denies command execution based on comparison with `threshold` * denies command execution based on comparison with `threshold`
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';
class Police { class Police {
/** /**
* Create a ratelimiter instance. * Create a ratelimiter instance.
*/ */
constructor () { constructor () {
this._records = {}; this._records = {};
this._halflife = 30000; // ms this._halflife = 30000; // ms
this._threshold = 25; this._threshold = 25;
} }
/** /**
* Finds current score by `id` * Finds current score by `id`
* *
* @param {String} id target id / address * @param {String} id target id / address
* @public * @public
* *
* @memberof Police * @memberof Police
*/ */
search (id) { search (id) {
let record = this._records[id]; let record = this._records[id];
if (!record) { if (!record) {
record = this._records[id] = { record = this._records[id] = {
time: Date.now(), time: Date.now(),
score: 0 score: 0
} }
} }
return record; return record;
} }
/** /**
* Adjusts the current ratelimit score by `deltaScore` * Adjusts the current ratelimit score by `deltaScore`
* *
* @param {String} id target id / address * @param {String} id target id / address
* @param {Number} deltaScore amount to adjust current score by * @param {Number} deltaScore amount to adjust current score by
* @public * @public
* *
* @memberof Police * @memberof Police
*/ */
frisk (id, deltaScore) { frisk (id, deltaScore) {
let record = this.search(id); let record = this.search(id);
if (record.arrested) { if (record.arrested) {
return true; return true;
} }
record.score *= Math.pow(2, -(Date.now() - record.time ) / this._halflife); record.score *= Math.pow(2, -(Date.now() - record.time ) / this._halflife);
record.score += deltaScore; record.score += deltaScore;
record.time = Date.now(); record.time = Date.now();
if (record.score >= this._threshold) { if (record.score >= this._threshold) {
return true; return true;
} }
return false; return false;
} }
/** /**
* Statically set server to no longer accept traffic from `id` * Statically set server to no longer accept traffic from `id`
* *
* @param {String} id target id / address * @param {String} id target id / address
* @public * @public
* *
* @memberof Police * @memberof Police
*/ */
arrest (id) { arrest (id) {
var record = this.search(id); var record = this.search(id);
if (record) { if (record) {
record.arrested = true; record.arrested = true;
} }
} }
/** /**
* Remove statically assigned limit from `id` * Remove statically assigned limit from `id`
* *
* @param {String} id target id / address * @param {String} id target id / address
* @public * @public
* *
* @memberof Police * @memberof Police
*/ */
pardon (id) { pardon (id) {
var record = this.search(id); var record = this.search(id);
if (record) { if (record) {
record.arrested = false; record.arrested = false;
} }
} }
} }
module.exports = Police; module.exports = Police;

View File

@ -1,11 +1,11 @@
/** /**
* Main websocket server handling communications and connection events * Main websocket server handling communications and connection events
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';
@ -13,185 +13,185 @@ const wsServer = require('ws').Server;
const Police = require('./rateLimiter'); const Police = require('./rateLimiter');
class server extends wsServer { class server extends wsServer {
/** /**
* Create a HackChat server instance. * Create a HackChat server instance.
* *
* @param {Object} core Reference to the core server object * @param {Object} core Reference to the core server object
*/ */
constructor (core) { constructor (core) {
super({ port: core.config.websocketPort }); super({ port: core.config.websocketPort });
this._core = core; this._core = core;
this._police = new Police(); this._police = new Police();
this._cmdBlacklist = {}; this._cmdBlacklist = {};
this.on('error', (err) => { this.on('error', (err) => {
this.handleError('server', err); this.handleError('server', err);
}); });
this.on('connection', (socket, request) => { this.on('connection', (socket, request) => {
this.newConnection(socket, request); this.newConnection(socket, request);
}); });
} }
/** /**
* Bind listeners for the new socket created on connection to this class * Bind listeners for the new socket created on connection to this class
* *
* @param {Object} socket New socket object * @param {Object} socket New socket object
* @param {Object} request Initial headers of the new connection * @param {Object} request Initial headers of the new connection
*/ */
newConnection (socket, request) { newConnection (socket, request) {
socket.remoteAddress = request.headers['x-forwarded-for'] || request.connection.remoteAddress; socket.remoteAddress = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
socket.on('message', ((data) => { socket.on('message', ((data) => {
this.handleData(socket, data); this.handleData(socket, data);
}).bind(this)); }).bind(this));
socket.on('close', (() => { socket.on('close', (() => {
this.handleClose(socket); this.handleClose(socket);
}).bind(this)); }).bind(this));
socket.on('error', ((err) => { socket.on('error', ((err) => {
this.handleError(socket, err); this.handleError(socket, err);
}).bind(this)); }).bind(this));
} }
/** /**
* Handle incoming messages from clients, parse and check command, then hand-off * Handle incoming messages from clients, parse and check command, then hand-off
* *
* @param {Object} socket Calling socket object * @param {Object} socket Calling socket object
* @param {String} data Message sent from client * @param {String} data Message sent from client
*/ */
handleData (socket, data) { handleData (socket, data) {
// TODO: Rate limit here // TODO: Rate limit here
// Don't penalize yet, but check whether IP is rate-limited // Don't penalize yet, but check whether IP is rate-limited
if (this._police.frisk(socket.remoteAddress, 0)) { if (this._police.frisk(socket.remoteAddress, 0)) {
this.reply({ cmd: 'warn', text: "Your IP is being rate-limited or blocked." }, socket); this.reply({ cmd: 'warn', text: "Your IP is being rate-limited or blocked." }, socket);
return; return;
} }
// Penalize here, but don't do anything about it // Penalize here, but don't do anything about it
this._police.frisk(socket.remoteAddress, 1); this._police.frisk(socket.remoteAddress, 1);
// ignore ridiculously large packets // ignore ridiculously large packets
if (data.length > 65536) { if (data.length > 65536) {
return; return;
} }
var args = null; var args = null;
try { try {
args = JSON.parse(data); args = JSON.parse(data);
} catch (e) { } catch (e) {
// Client sent malformed json, gtfo // Client sent malformed json, gtfo
socket.close(); socket.close();
} }
if (args === null) if (args === null)
return; return;
if (typeof args.cmd === 'undefined' || args.cmd == 'ping') if (typeof args.cmd === 'undefined' || args.cmd == 'ping')
return; return;
var cmd = args.cmd; var cmd = args.cmd;
if (typeof socket.channel === 'undefined' && cmd !== 'join') if (typeof socket.channel === 'undefined' && cmd !== 'join')
return; return;
if (typeof this._cmdBlacklist[cmd] === 'function') { if (typeof this._cmdBlacklist[cmd] === 'function') {
return; return;
} }
this._core.commands.handleCommand(this, socket, args); this._core.commands.handleCommand(this, socket, args);
} }
/** /**
* Handle socket close from clients * Handle socket close from clients
* *
* @param {Object} socket Closing socket object * @param {Object} socket Closing socket object
*/ */
handleClose (socket) { handleClose (socket) {
try { try {
if (socket.channel) { if (socket.channel) {
this.broadcast({ this.broadcast({
cmd: 'onlineRemove', cmd: 'onlineRemove',
nick: socket.nick nick: socket.nick
}, { channel: socket.channel }); }, { channel: socket.channel });
} }
} catch (e) { } catch (e) {
// TODO: Should this be added to the error log? // TODO: Should this be added to the error log?
} }
} }
/** /**
* "Handle" server or socket errors * "Handle" server or socket errors
* *
* @param {Object||String} socket Calling socket object, or 'server' * @param {Object||String} socket Calling socket object, or 'server'
* @param {String} err The sad stuff * @param {String} err The sad stuff
*/ */
handleError (socket, err) { handleError (socket, err) {
// Meh, yolo // Meh, yolo
// I mean; // I mean;
// TODO: Should this be added to the error log? // TODO: Should this be added to the error log?
} }
/** /**
* Send data payload to specific socket/client * Send data payload to specific socket/client
* *
* @param {Object} data Object to convert to json for transmission * @param {Object} data Object to convert to json for transmission
* @param {Object} socket The target client * @param {Object} socket The target client
*/ */
send (data, socket) { send (data, socket) {
// Add timestamp to command // Add timestamp to command
data.time = Date.now(); data.time = Date.now();
try { try {
if (socket.readyState == 1) { // Who says statically checking port status is bad practice? Everyone? Damnit. #TODO if (socket.readyState == 1) { // Who says statically checking port status is bad practice? Everyone? Damnit. #TODO
socket.send(JSON.stringify(data)); socket.send(JSON.stringify(data));
} }
} catch (e) { } } catch (e) { }
} }
/** /**
* Overload function for `this.send()` * Overload function for `this.send()`
* *
* @param {Object} data Object to convert to json for transmission * @param {Object} data Object to convert to json for transmission
* @param {Object} socket The target client * @param {Object} socket The target client
*/ */
reply (data, socket) { reply (data, socket) {
this.send(data, socket); this.send(data, socket);
} }
/** /**
* Finds sockets/clients that meet the filter requirements, then passes the data to them * Finds sockets/clients that meet the filter requirements, then passes the data to them
* *
* @param {Object} data Object to convert to json for transmission * @param {Object} data Object to convert to json for transmission
* @param {Object} filter The socket must of equal or greater attribs matching `filter` * @param {Object} filter The socket must of equal or greater attribs matching `filter`
* = {} // matches all * = {} // matches all
* = { channel: 'programming' } // matches any socket where (`socket.channel` === 'programming') * = { channel: 'programming' } // matches any socket where (`socket.channel` === 'programming')
* = { channel: 'programming', nick: 'Marzavec' } // matches any socket where (`socket.channel` === 'programming' && `socket.nick` === 'Marzavec') * = { channel: 'programming', nick: 'Marzavec' } // matches any socket where (`socket.channel` === 'programming' && `socket.nick` === 'Marzavec')
*/ */
broadcast (data, filter) { broadcast (data, filter) {
let filterAttribs = Object.keys(filter); let filterAttribs = Object.keys(filter);
let reqCount = filterAttribs.length; let reqCount = filterAttribs.length;
let curMatch; let curMatch;
let sent = false; let sent = false;
for ( let socket of this.clients ) { for ( let socket of this.clients ) {
curMatch = 0; curMatch = 0;
for( let i = 0; i < reqCount; i++ ) { for( let i = 0; i < reqCount; i++ ) {
if (typeof socket[filterAttribs[i]] !== 'undefined' && socket[filterAttribs[i]] === filter[filterAttribs[i]]) if (typeof socket[filterAttribs[i]] !== 'undefined' && socket[filterAttribs[i]] === filter[filterAttribs[i]])
curMatch++; curMatch++;
} }
if (curMatch === reqCount) { if (curMatch === reqCount) {
this.send(data, socket); this.send(data, socket);
sent = true; sent = true;
} }
} }
return sent; return sent;
} }
} }
module.exports = server; module.exports = server;

View File

@ -1,11 +1,11 @@
/** /**
* Commands / protocol manager- loads, validates and handles command execution * Commands / protocol manager- loads, validates and handles command execution
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';
@ -14,231 +14,231 @@ const chalk = require('chalk');
const didYouMean = require('didyoumean2'); const didYouMean = require('didyoumean2');
class CommandManager { class CommandManager {
/** /**
* Create a `CommandManager` instance for handling commands/protocol * Create a `CommandManager` instance for handling commands/protocol
* *
* @param {Object} core reference to the global core object * @param {Object} core reference to the global core object
*/ */
constructor (core) { constructor (core) {
this.core = core; this.core = core;
this._commands = []; this._commands = [];
this._categories = []; this._categories = [];
} }
/** /**
* (Re)initializes name spaces for commands and starts load routine * (Re)initializes name spaces for commands and starts load routine
* *
*/ */
loadCommands () { loadCommands () {
this._commands = []; this._commands = [];
this._categories = []; this._categories = [];
const core = this.core; const core = this.core;
const commandImports = core.managers.dynamicImports.getImport('src/commands'); const commandImports = core.managers.dynamicImports.getImport('src/commands');
let cmdErrors = ''; let cmdErrors = '';
Object.keys(commandImports).forEach(file => { Object.keys(commandImports).forEach(file => {
let command = commandImports[file]; let command = commandImports[file];
let name = path.basename(file); let name = path.basename(file);
cmdErrors += this._validateAndLoad(command, file, name); cmdErrors += this._validateAndLoad(command, file, name);
}); });
return cmdErrors; return cmdErrors;
} }
/** /**
* Checks the module after having been `require()`ed in and reports errors * Checks the module after having been `require()`ed in and reports errors
* *
* @param {Object} command reference to the newly loaded object * @param {Object} command reference to the newly loaded object
* @param {String} file file path to the module * @param {String} file file path to the module
* @param {String} name command (`cmd`) name * @param {String} name command (`cmd`) name
*/ */
_validateAndLoad (command, file, name) { _validateAndLoad (command, file, name) {
let error = this._validateCommand(command); let error = this._validateCommand(command);
if (error) { if (error) {
// TODO: Add to logger? // TODO: Add to logger?
let errText = `Failed to load '${name}': ${error}\n\n`; let errText = `Failed to load '${name}': ${error}\n\n`;
console.log(errText); console.log(errText);
return errText; return errText;
} }
if (!command.category) { if (!command.category) {
let base = path.join(this.core.managers.dynamicImports.base, 'commands'); let base = path.join(this.core.managers.dynamicImports.base, 'commands');
let category = 'Uncategorized'; let category = 'Uncategorized';
if (file.indexOf(path.sep) > -1) { if (file.indexOf(path.sep) > -1) {
category = path.dirname(path.relative(base, file)) category = path.dirname(path.relative(base, file))
.replace(new RegExp(path.sep.replace('\\', '\\\\'), 'g'), '/'); .replace(new RegExp(path.sep.replace('\\', '\\\\'), 'g'), '/');
} }
command.info.category = category; command.info.category = category;
if (this._categories.indexOf(category) === -1) if (this._categories.indexOf(category) === -1)
this._categories.push(category); this._categories.push(category);
} }
if (typeof command.init === 'function') { if (typeof command.init === 'function') {
try { try {
command.init(this.core); command.init(this.core);
} catch (err) { } catch (err) {
// TODO: Add to logger? // TODO: Add to logger?
let errText = `Failed to initialize '${name}': ${err}\n\n`; let errText = `Failed to initialize '${name}': ${err}\n\n`;
console.log(errText); console.log(errText);
return errText; return errText;
} }
} }
this._commands.push(command); this._commands.push(command);
return ''; return '';
} }
/** /**
* Checks the module after having been `require()`ed in and reports errors * Checks the module after having been `require()`ed in and reports errors
* *
* @param {Object} object reference to the newly loaded object * @param {Object} object reference to the newly loaded object
*/ */
_validateCommand (object) { _validateCommand (object) {
if (typeof object !== 'object') if (typeof object !== 'object')
return 'command setup is invalid'; return 'command setup is invalid';
if (typeof object.run !== 'function') if (typeof object.run !== 'function')
return 'run function is missing'; return 'run function is missing';
if (typeof object.info !== 'object') if (typeof object.info !== 'object')
return 'info object is missing'; return 'info object is missing';
if (typeof object.info.name !== 'string') if (typeof object.info.name !== 'string')
return 'info object is missing a valid name field'; return 'info object is missing a valid name field';
return null; return null;
} }
/** /**
* Pulls all command names from a passed `category` * Pulls all command names from a passed `category`
* *
* @param {String} category reference to the newly loaded object * @param {String} category reference to the newly loaded object
*/ */
all (category) { all (category) {
return !category ? this._commands : this._commands.filter(c => c.info.category.toLowerCase() === category.toLowerCase()); return !category ? this._commands : this._commands.filter(c => c.info.category.toLowerCase() === category.toLowerCase());
} }
/** /**
* Pulls all category names * Pulls all category names
* *
*/ */
categories () { categories () {
return this._categories; return this._categories;
} }
/** /**
* Pulls command by name or alia(s) * Pulls command by name or alia(s)
* *
* @param {String} name name or alias of command * @param {String} name name or alias of command
*/ */
get (name) { get (name) {
return this.findBy('name', name) return this.findBy('name', name)
|| this._commands.find(command => command.info.aliases instanceof Array && command.info.aliases.indexOf(name) > -1); || this._commands.find(command => command.info.aliases instanceof Array && command.info.aliases.indexOf(name) > -1);
} }
/** /**
* Pulls command by arbitrary search of the `module.info` attribute * Pulls command by arbitrary search of the `module.info` attribute
* *
* @param {String} key name or alias of command * @param {String} key name or alias of command
* @param {String} value name or alias of command * @param {String} value name or alias of command
*/ */
findBy (key, value) { findBy (key, value) {
return this._commands.find(c => c.info[key] === value); return this._commands.find(c => c.info[key] === value);
} }
/** /**
* Finds and executes the requested command, or fails with semi-intelligent error * Finds and executes the requested command, or fails with semi-intelligent error
* *
* @param {Object} server main server reference * @param {Object} server main server reference
* @param {Object} socket calling socket reference * @param {Object} socket calling socket reference
* @param {Object} data command structure passed by socket (client) * @param {Object} data command structure passed by socket (client)
*/ */
handleCommand (server, socket, data) { handleCommand (server, socket, data) {
// Try to find command first // Try to find command first
let command = this.get(data.cmd); let command = this.get(data.cmd);
if (command) { if (command) {
return this.execute(command, server, socket, data); return this.execute(command, server, socket, data);
} else { } else {
// Then fail with helpful (sorta) message // Then fail with helpful (sorta) message
return this._handleFail(server, socket, data); return this._handleFail(server, socket, data);
} }
} }
/** /**
* Requested command failure handler, attempts to find command and reports back * Requested command failure handler, attempts to find command and reports back
* *
* @param {Object} server main server reference * @param {Object} server main server reference
* @param {Object} socket calling socket reference * @param {Object} socket calling socket reference
* @param {Object} data command structure passed by socket (client) * @param {Object} data command structure passed by socket (client)
*/ */
_handleFail(server, socket, data) { _handleFail(server, socket, data) {
const maybe = didYouMean(data.cmd, this.all().map(c => c.info.name), { const maybe = didYouMean(data.cmd, this.all().map(c => c.info.name), {
threshold: 5, threshold: 5,
thresholdType: 'edit-distance' thresholdType: 'edit-distance'
}); });
if (maybe) { if (maybe) {
// Found a suggestion, pass it on to their dyslexic self // Found a suggestion, pass it on to their dyslexic self
return server.reply({ return server.reply({
cmd: 'warn', cmd: 'warn',
text: `Command not found, did you mean: \`${maybe}\`?` text: `Command not found, did you mean: \`${maybe}\`?`
}, socket); }, socket);
} }
// Request so mangled that I don't even, silently fail // Request so mangled that I don't even, silently fail
return; return;
} }
/** /**
* Attempt to execute the requested command, fail if err or bad params * Attempt to execute the requested command, fail if err or bad params
* *
* @param {Object} command target command module * @param {Object} command target command module
* @param {Object} server main server reference * @param {Object} server main server reference
* @param {Object} socket calling socket reference * @param {Object} socket calling socket reference
* @param {Object} data command structure passed by socket (client) * @param {Object} data command structure passed by socket (client)
*/ */
async execute(command, server, socket, data) { async execute(command, server, socket, data) {
if (typeof command.requiredData !== 'undefined') { if (typeof command.requiredData !== 'undefined') {
let missing = []; let missing = [];
for (let i = 0, len = command.requiredData.length; i < len; i++) { for (let i = 0, len = command.requiredData.length; i < len; i++) {
if (typeof data[command.requiredData[i]] === 'undefined') if (typeof data[command.requiredData[i]] === 'undefined')
missing.push(command.requiredData[i]); missing.push(command.requiredData[i]);
} }
if (missing.length > 0) { if (missing.length > 0) {
let errText = `Failed to execute '${command.info.name}': missing required ${missing.join(', ')}\n\n`; let errText = `Failed to execute '${command.info.name}': missing required ${missing.join(', ')}\n\n`;
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: errText text: errText
}, socket); }, socket);
return null; return null;
} }
} }
try { try {
return await command.run(this.core, server, socket, data); return await command.run(this.core, server, socket, data);
} catch (err) { } catch (err) {
// TODO: Add to logger? // TODO: Add to logger?
let errText = `Failed to execute '${command.info.name}': ${err}\n\n`; let errText = `Failed to execute '${command.info.name}': ${err}\n\n`;
console.log(errText); console.log(errText);
server.reply({ server.reply({
cmd: 'warn', cmd: 'warn',
text: errText text: errText
}, socket); }, socket);
return null; return null;
} }
} }
} }
module.exports = CommandManager; module.exports = CommandManager;

View File

@ -1,12 +1,12 @@
/** /**
* Server configuration manager, handling loading, creation, parsing and saving * Server configuration manager, handling loading, creation, parsing and saving
* of the main config.json file * of the main config.json file
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';
@ -19,210 +19,210 @@ const path = require('path');
const deSync = require('deasync'); const deSync = require('deasync');
class ConfigManager { class ConfigManager {
/** /**
* Create a `ConfigManager` instance for (re)loading classes and config * Create a `ConfigManager` instance for (re)loading classes and config
* *
* @param {Object} core reference to the global core object * @param {Object} core reference to the global core object
* @param {String} base executing directory name; __dirname * @param {String} base executing directory name; __dirname
* @param {Object} dynamicImports dynamic import engine reference * @param {Object} dynamicImports dynamic import engine reference
*/ */
constructor (core, base, dynamicImports) { constructor (core, base, dynamicImports) {
this._core = core; this._core = core;
this._base = base; this._base = base;
this._configPath = path.resolve(base, 'config/config.json'); this._configPath = path.resolve(base, 'config/config.json');
this._dynamicImports = dynamicImports; this._dynamicImports = dynamicImports;
} }
/** /**
* Pulls both core config questions along with any optional config questions, * Pulls both core config questions along with any optional config questions,
* used in building the initial config.json or re-building it. * used in building the initial config.json or re-building it.
* *
* @param {Object} currentConfig an object containing current server settings, if any * @param {Object} currentConfig an object containing current server settings, if any
* @param {Object} optionalConfigs optional (non-core) module config * @param {Object} optionalConfigs optional (non-core) module config
*/ */
getQuestions (currentConfig, optionalConfigs) { getQuestions (currentConfig, optionalConfigs) {
// core server setup questions // core server setup questions
const questions = { const questions = {
properties: { properties: {
adminName: { adminName: {
pattern: /^"?[a-zA-Z0-9_]+"?$/, pattern: /^"?[a-zA-Z0-9_]+"?$/,
type: 'string', type: 'string',
message: 'Nicks can only contain letters, numbers and underscores', message: 'Nicks can only contain letters, numbers and underscores',
required: !currentConfig.adminName, required: !currentConfig.adminName,
default: currentConfig.adminName, default: currentConfig.adminName,
before: value => value.replace(/"/g, '') before: value => value.replace(/"/g, '')
}, },
adminPass: { adminPass: {
type: 'string', type: 'string',
required: !currentConfig.adminPass, required: !currentConfig.adminPass,
default: currentConfig.adminPass, default: currentConfig.adminPass,
hidden: true, hidden: true,
replace: '*', replace: '*',
}, },
websocketPort: { websocketPort: {
type: 'number', type: 'number',
required: !currentConfig.websocketPort, required: !currentConfig.websocketPort,
default: currentConfig.websocketPort || 6060 default: currentConfig.websocketPort || 6060
}, },
tripSalt: { tripSalt: {
type: 'string', type: 'string',
required: !currentConfig.tripSalt, required: !currentConfig.tripSalt,
default: currentConfig.tripSalt, default: currentConfig.tripSalt,
hidden: true, hidden: true,
replace: '*', replace: '*',
} }
} }
}; };
// non-core server setup questions, for future plugin support // non-core server setup questions, for future plugin support
Object.keys(optionalConfigs).forEach(configName => { Object.keys(optionalConfigs).forEach(configName => {
const config = optionalConfigs[configName]; const config = optionalConfigs[configName];
const question = config.getQuestion(currentConfig, configName); const question = config.getQuestion(currentConfig, configName);
if (!question) { if (!question) {
return; return;
} }
question.description = (question.description || configName) + ' (Optional)'; question.description = (question.description || configName) + ' (Optional)';
questions.properties[configName] = question; questions.properties[configName] = question;
}); });
return questions; return questions;
} }
/** /**
* `load` function overload, only blocking * `load` function overload, only blocking
* *
*/ */
loadSync () { loadSync () {
let conf = {}; let conf = {};
conf = this.load(); conf = this.load();
// trip salt is the last core config question, wait until it's been populated // trip salt is the last core config question, wait until it's been populated
// TODO: update this to work with new plugin support // TODO: update this to work with new plugin support
while(conf === null || typeof conf.tripSalt === 'undefined') { while(conf === null || typeof conf.tripSalt === 'undefined') {
deSync.sleep(100); deSync.sleep(100);
} }
return conf; return conf;
} }
/** /**
* (Re)builds the config.json (main server config), or loads the config into mem * (Re)builds the config.json (main server config), or loads the config into mem
* if rebuilding, process will exit- this is to allow a process manager to take over * if rebuilding, process will exit- this is to allow a process manager to take over
* *
* @param {Boolean} reconfiguring set to true by `scripts/configure.js`, will exit if true * @param {Boolean} reconfiguring set to true by `scripts/configure.js`, will exit if true
*/ */
load (reconfiguring = false) { load (reconfiguring = false) {
if (reconfiguring || !fse.existsSync(this._configPath)) { if (reconfiguring || !fse.existsSync(this._configPath)) {
// gotta have that sexy console // gotta have that sexy console
console.log(stripIndents` console.log(stripIndents`
${chalk.magenta('°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸°º¤ø,¸¸,ø¤º°`°º¤ø')} ${chalk.magenta('°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸°º¤ø,¸¸,ø¤º°`°º¤ø')}
${chalk.gray('--------------(') + chalk.white(' HackChat Setup Wizard v1.0 ') + chalk.gray(')--------------')} ${chalk.gray('--------------(') + chalk.white(' HackChat Setup Wizard v1.0 ') + chalk.gray(')--------------')}
${chalk.magenta('°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸°º¤ø,¸¸,ø¤º°`°º¤ø')} ${chalk.magenta('°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸°º¤ø,¸¸,ø¤º°`°º¤ø')}
For advanced setup, see the HackChat wiki at: For advanced setup, see the HackChat wiki at:
${chalk.green('https://github.com/')} ${chalk.green('https://github.com/')}
${chalk.white('Note:')} ${chalk.green('npm/yarn run config')} will re-run this utility. ${chalk.white('Note:')} ${chalk.green('npm/yarn run config')} will re-run this utility.
You will now be asked for the following: You will now be asked for the following:
- ${chalk.magenta('Admin Name')}, the initial admin username - ${chalk.magenta('Admin Name')}, the initial admin username
- ${chalk.magenta('Admin Pass')}, the initial admin password - ${chalk.magenta('Admin Pass')}, the initial admin password
- ${chalk.magenta(' Port')}, the port for the websocket - ${chalk.magenta(' Port')}, the port for the websocket
- ${chalk.magenta(' Salt')}, the salt for username trip - ${chalk.magenta(' Salt')}, the salt for username trip
\u200b \u200b
`); `);
let currentConfig = this._config || {}; let currentConfig = this._config || {};
if (reconfiguring && fse.existsSync(this._configPath)) { if (reconfiguring && fse.existsSync(this._configPath)) {
this._backup(); this._backup();
currentConfig = fse.readJSONSync(this._configPath); currentConfig = fse.readJSONSync(this._configPath);
} }
prompt.get(this.getQuestions(currentConfig, this._dynamicImports.optionalConfigs), (err, res) => { prompt.get(this.getQuestions(currentConfig, this._dynamicImports.optionalConfigs), (err, res) => {
if (typeof res.mods === 'undefined') { if (typeof res.mods === 'undefined') {
res.mods = []; res.mods = [];
} }
if (err) { if (err) {
console.error(err); console.error(err);
process.exit(666); // SPOOKY! process.exit(666); // SPOOKY!
} }
try { try {
fse.outputJsonSync(this._configPath, res); fse.outputJsonSync(this._configPath, res);
} catch (e) { } catch (e) {
console.error(`Couldn't write config to ${this._configPath}\n${e.stack}`); console.error(`Couldn't write config to ${this._configPath}\n${e.stack}`);
if (!reconfiguring) { if (!reconfiguring) {
process.exit(666); // SPOOKY! process.exit(666); // SPOOKY!
} }
} }
console.log('Config generated! You may now start the server normally.') console.log('Config generated! You may now start the server normally.')
process.exit(reconfiguring ? 0 : 42); process.exit(reconfiguring ? 0 : 42);
}); });
return null; return null;
} }
this._config = fse.readJSONSync(this._configPath); this._config = fse.readJSONSync(this._configPath);
return this._config; return this._config;
} }
/** /**
* Creates backup of current config into _configPath * Creates backup of current config into _configPath
* *
*/ */
_backup () { _backup () {
const backupPath = `${this._configPath}.${dateFormat('dd-mm-yy-HH-MM-ss')}.bak`; const backupPath = `${this._configPath}.${dateFormat('dd-mm-yy-HH-MM-ss')}.bak`;
fse.copySync(this._configPath, backupPath); fse.copySync(this._configPath, backupPath);
return backupPath; return backupPath;
} }
/** /**
* First makes a backup of the current `config.json`, then writes current config * First makes a backup of the current `config.json`, then writes current config
* to disk * to disk
* *
*/ */
save () { save () {
const backupPath = this._backup(); const backupPath = this._backup();
if (!fse.existsSync(this._configPath)){ if (!fse.existsSync(this._configPath)){
fse.mkdirSync(this._configPath); fse.mkdirSync(this._configPath);
} }
try { try {
fse.writeJSONSync(this._configPath, this._config); fse.writeJSONSync(this._configPath, this._config);
fse.removeSync(backupPath); fse.removeSync(backupPath);
return true; return true;
} catch (e) { } catch (e) {
// TODO: restore backup // TODO: restore backup
// TODO: output to logging engine? // TODO: output to logging engine?
console.log('Failed to save config file!'); console.log('Failed to save config file!');
return false; return false;
} }
} }
/** /**
* Updates current config[`key`] with `value` then writes changes to disk * Updates current config[`key`] with `value` then writes changes to disk
* *
* @param {*} key arbitrary configuration key * @param {*} key arbitrary configuration key
* @param {*} value new value to change `key` to * @param {*} value new value to change `key` to
*/ */
set (key, value) { set (key, value) {
const realKey = `${key}`; const realKey = `${key}`;
this._config[realKey] = value; this._config[realKey] = value;
this.save(); this.save();
} }
} }
module.exports = ConfigManager; module.exports = ConfigManager;

View File

@ -1,11 +1,11 @@
/** /**
* Import managment base, used to load commands/protocol and configuration objects * Import managment base, used to load commands/protocol and configuration objects
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';
@ -13,135 +13,135 @@ const read = require('readdir-recursive');
const path = require('path'); const path = require('path');
class ImportsManager { class ImportsManager {
/** /**
* Create a `ImportsManager` instance for (re)loading classes and config * Create a `ImportsManager` instance for (re)loading classes and config
* *
* @param {Object} core reference to the global core object * @param {Object} core reference to the global core object
* @param {String} base executing directory name; __dirname * @param {String} base executing directory name; __dirname
*/ */
constructor (core, base) { constructor (core, base) {
this._core = core; this._core = core;
this._base = base; this._base = base;
this._imports = {}; this._imports = {};
this._optionalConfigs = {}; this._optionalConfigs = {};
} }
/** /**
* Pull core reference * Pull core reference
* *
* @type {Object} readonly * @type {Object} readonly
*/ */
get core () { get core () {
return this._core; return this._core;
} }
/** /**
* Pull base path that all imports are required in from * Pull base path that all imports are required in from
* *
* @type {String} readonly * @type {String} readonly
*/ */
get base () { get base () {
return this._base; return this._base;
} }
/** /**
* Pull optional (none-core) config options * Pull optional (none-core) config options
* *
* @type {Object} * @type {Object}
*/ */
get optionalConfigs () { get optionalConfigs () {
return Object.assign({}, this._optionalConfigs); return Object.assign({}, this._optionalConfigs);
} }
/** /**
* Initialize this class and start loading target directories * Initialize this class and start loading target directories
* *
*/ */
init () { init () {
let errorText = ''; let errorText = '';
ImportsManager.load_dirs.forEach(dir => { ImportsManager.load_dirs.forEach(dir => {
errorText += this.loadDir(dir); errorText += this.loadDir(dir);
}); });
return errorText; return errorText;
} }
/** /**
* Gather all js files from target directory, then verify and load * Gather all js files from target directory, then verify and load
* *
* @param {String} dirName The name of the dir to load, relative to the _base path. * @param {String} dirName The name of the dir to load, relative to the _base path.
*/ */
loadDir (dirName) { loadDir (dirName) {
const dir = path.resolve(this._base, dirName); const dir = path.resolve(this._base, dirName);
let errorText = ''; let errorText = '';
try { try {
read.fileSync(dir).forEach(file => { read.fileSync(dir).forEach(file => {
const basename = path.basename(file); const basename = path.basename(file);
if (basename.startsWith('_') || !basename.endsWith('.js')) return; if (basename.startsWith('_') || !basename.endsWith('.js')) return;
let imported; let imported;
try { try {
imported = require(file); imported = require(file);
} catch (e) { } catch (e) {
let err = `Unable to load modules from ${dirName} (${path.relative(dir, file)})\n${e}`; let err = `Unable to load modules from ${dirName} (${path.relative(dir, file)})\n${e}`;
errorText += err; errorText += err;
console.error(err); console.error(err);
return errorText; return errorText;
} }
if (imported.configs) { if (imported.configs) {
imported.configs.forEach(config => { imported.configs.forEach(config => {
this._optionalConfigs[config.name] = config; this._optionalConfigs[config.name] = config;
}); });
} }
if (!this._imports[dirName]) { if (!this._imports[dirName]) {
this._imports[dirName] = {}; this._imports[dirName] = {};
} }
this._imports[dirName][file] = imported; this._imports[dirName][file] = imported;
}); });
} catch (e) { } catch (e) {
let err = `Unable to load modules from ${dirName}\n${e}`; let err = `Unable to load modules from ${dirName}\n${e}`;
errorText += err; errorText += err;
console.error(err); console.error(err);
return errorText; return errorText;
} }
return errorText; return errorText;
} }
/** /**
* Unlink references to each loaded module, pray to google that gc knows it's job, * Unlink references to each loaded module, pray to google that gc knows it's job,
* then reinitialize this class to start the reload * then reinitialize this class to start the reload
* *
* @param {String} dirName The name of the dir to load, relative to the _base path. * @param {String} dirName The name of the dir to load, relative to the _base path.
*/ */
reloadDirCache (dirName) { reloadDirCache (dirName) {
Object.keys(this._imports[dirName]).forEach((mod) => { Object.keys(this._imports[dirName]).forEach((mod) => {
delete require.cache[require.resolve(mod)]; delete require.cache[require.resolve(mod)];
}); });
return this.init(); return this.init();
} }
/** /**
* Pull reference to imported modules that were imported from dirName, or * Pull reference to imported modules that were imported from dirName, or
* load required directory if not found * load required directory if not found
* *
* @param {String} dirName The name of the dir to load, relative to the _base path. * @param {String} dirName The name of the dir to load, relative to the _base path.
*/ */
getImport (dirName) { getImport (dirName) {
let imported = this._imports[dirName]; let imported = this._imports[dirName];
if (!imported) { if (!imported) {
this.loadDir(dirName); this.loadDir(dirName);
} }
return Object.assign({}, this._imports[dirName]); return Object.assign({}, this._imports[dirName]);
} }
} }
// automagically loaded directorys on instantiation // automagically loaded directorys on instantiation

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
CommandManager: require('./commands'), CommandManager: require('./commands'),
Config: require('./config'), Config: require('./config'),
ImportsManager: require('./imports-manager'), ImportsManager: require('./imports-manager'),
Stats: require('./stats') Stats: require('./stats')
}; };

View File

@ -1,61 +1,61 @@
/** /**
* Simple generic stats collection script for events occurances (etc) * Simple generic stats collection script for events occurances (etc)
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';
class Stats { class Stats {
/** /**
* Create a stats instance. * Create a stats instance.
* *
*/ */
constructor () { constructor () {
this._stats = {}; this._stats = {};
} }
/** /**
* Retrieve value of arbitrary `key` reference * Retrieve value of arbitrary `key` reference
* *
* @param {String} key Reference to the arbitrary store name * @param {String} key Reference to the arbitrary store name
*/ */
get (key) { get (key) {
return this._stats[key]; return this._stats[key];
} }
/** /**
* Set value of arbitrary `key` reference * Set value of arbitrary `key` reference
* *
* @param {String} key Reference to the arbitrary store name * @param {String} key Reference to the arbitrary store name
* @param {Number} value New value for `key` * @param {Number} value New value for `key`
*/ */
set (key, value) { set (key, value) {
this._stats[key] = value; this._stats[key] = value;
} }
/** /**
* Increase value of arbitrary `key` reference, by 1 or `amount` * Increase value of arbitrary `key` reference, by 1 or `amount`
* *
* @param {String} key Reference to the arbitrary store name * @param {String} key Reference to the arbitrary store name
* @param {Number} amount Value to increase `key` by, or 1 if omitted * @param {Number} amount Value to increase `key` by, or 1 if omitted
*/ */
increment (key, amount) { increment (key, amount) {
this.set(key, (this.get(key) || 0) + (amount || 1)); this.set(key, (this.get(key) || 0) + (amount || 1));
} }
/** /**
* Reduce value of arbitrary `key` reference, by 1 or `amount` * Reduce value of arbitrary `key` reference, by 1 or `amount`
* *
* @param {String} key Reference to the arbitrary store name * @param {String} key Reference to the arbitrary store name
* @param {Number} amount Value to decrease `key` by, or 1 if omitted * @param {Number} amount Value to decrease `key` by, or 1 if omitted
*/ */
decrement (key, amount) { decrement (key, amount) {
this.set(key, (this.get(key) || 0) - (amount || 1)); this.set(key, (this.get(key) || 0) - (amount || 1));
} }
} }
module.exports = Stats; module.exports = Stats;

View File

@ -1,11 +1,11 @@
/** /**
* Server configuration script, used reconfiguring server options * Server configuration script, used reconfiguring server options
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';

View File

@ -1,11 +1,11 @@
/** /**
* Server debug test script * Server debug test script
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';

View File

@ -1,11 +1,11 @@
/** /**
* Server development script * Server development script
* *
* Version: v2.0.0 * Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec ) * Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) * License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
* *
*/ */
'use strict'; 'use strict';