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
*
* Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
*
*/
* HackChat main server entry point
*
* Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
*
*/
'use strict';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,52 +5,52 @@
'use strict';
exports.run = async (core, server, socket, data) => {
// process text
let text = String(data.text);
// 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");
if (!text) {
// lets not send empty text?
return;
}
// process text
let text = String(data.text);
// 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");
if (!text) {
// lets not send empty text?
return;
}
let score = text.length / 83 / 4;
if (server._police.frisk(socket.remoteAddress, score)) {
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 score = text.length / 83 / 4;
if (server._police.frisk(socket.remoteAddress, score)) {
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;
}
return;
}
let payload = {
cmd: 'chat',
nick: socket.nick,
text: text
};
let payload = {
cmd: 'chat',
nick: socket.nick,
text: text
};
if (socket.uType == 'admin') {
payload.admin = true;
} else if (socket.uType == 'mod') {
payload.mod = true;
}
if (socket.uType == 'admin') {
payload.admin = true;
} else if (socket.uType == 'mod') {
payload.mod = true;
}
if (socket.trip) {
payload.trip = socket.trip;
}
if (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.info = {
name: 'chat',
usage: 'chat {text}',
description: 'Broadcasts passed `text` field to the calling users channel'
name: 'chat',
usage: 'chat {text}',
description: 'Broadcasts passed `text` field to the calling users channel'
};

View File

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

View File

@ -5,60 +5,60 @@
'use strict';
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) => {
let targetNick = String(data.nick);
let targetNick = String(data.nick);
if (!verifyNickname(targetNick)) {
// Not a valid nickname? Chances are we won't find them
return;
}
if (!verifyNickname(targetNick)) {
// Not a valid nickname? Chances are we won't find them
return;
}
if (targetNick == socket.nick) {
// TODO: reply with something witty? They invited themself
return;
}
if (targetNick == socket.nick) {
// TODO: reply with something witty? They invited themself
return;
}
if (server._police.frisk(socket.remoteAddress, 2)) {
server.reply({
cmd: 'warn',
text: 'You are sending invites too fast. Wait a moment before trying again.'
}, socket);
if (server._police.frisk(socket.remoteAddress, 2)) {
server.reply({
cmd: 'warn',
text: 'You are sending invites too fast. Wait a moment before trying again.'
}, socket);
return;
}
return;
}
let channel = Math.random().toString(36).substr(2, 8);
let channel = Math.random().toString(36).substr(2, 8);
let payload = {
cmd: 'info',
text: `${socket.nick} invited you to ?${channel}`
};
let inviteSent = server.broadcast( payload, { channel: socket.channel, nick: targetNick });
let payload = {
cmd: 'info',
text: `${socket.nick} invited you to ?${channel}`
};
let inviteSent = server.broadcast( payload, { channel: socket.channel, nick: targetNick });
if (!inviteSent) {
server.reply({
cmd: 'warn',
text: 'Could not find user in channel'
}, socket);
if (!inviteSent) {
server.reply({
cmd: 'warn',
text: 'Could not find user in channel'
}, socket);
return;
}
return;
}
server.reply({
cmd: 'info',
text: `You invited ${targetNick} to ?${channel}`
}, socket);
server.reply({
cmd: 'info',
text: `You invited ${targetNick} to ?${channel}`
}, socket);
core.managers.stats.increment('invites-sent');
core.managers.stats.increment('invites-sent');
};
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'
name: 'invite',
usage: 'invite {nick}',
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');
function hash(password) {
var sha = crypto.createHash('sha256');
sha.update(password);
return sha.digest('base64').substr(0, 6);
var sha = crypto.createHash('sha256');
sha.update(password);
return sha.digest('base64').substr(0, 6);
}
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) => {
if (server._police.frisk(socket.remoteAddress, 3)) {
server.reply({
cmd: 'warn',
text: 'You are joining channels too fast. Wait a moment and try again.'
}, socket);
if (server._police.frisk(socket.remoteAddress, 3)) {
server.reply({
cmd: 'warn',
text: 'You are joining channels too fast. Wait a moment and try again.'
}, socket);
return;
}
return;
}
if (typeof socket.channel !== 'undefined') {
// Calling socket already in a channel
// TODO: allow changing of channel without reconnection
return;
}
if (typeof socket.channel !== 'undefined') {
// Calling socket already in a channel
// TODO: allow changing of channel without reconnection
return;
}
let channel = String(data.channel).trim();
if (!channel) {
// Must join a non-blank channel
return;
}
let channel = String(data.channel).trim();
if (!channel) {
// Must join a non-blank channel
return;
}
// Process nickname
let nick = String(data.nick);
let nickArray = nick.split('#', 2);
nick = nickArray[0].trim();
// Process nickname
let nick = String(data.nick);
let nickArray = nick.split('#', 2);
nick = nickArray[0].trim();
if (!verifyNickname(nick)) {
server.reply({
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores'
}, socket);
if (!verifyNickname(nick)) {
server.reply({
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores'
}, socket);
return
}
return
}
for (let client of server.clients) {
if (client.channel === channel) {
if (client.nick.toLowerCase() === nick.toLowerCase()) {
server.reply({
cmd: 'warn',
text: 'Nickname taken'
}, socket);
for (let client of server.clients) {
if (client.channel === channel) {
if (client.nick.toLowerCase() === nick.toLowerCase()) {
server.reply({
cmd: 'warn',
text: 'Nickname taken'
}, socket);
return;
}
}
}
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.reply({
cmd: 'warn',
text: 'Gtfo'
}, socket);
// 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.reply({
cmd: 'warn',
text: 'Gtfo'
}, socket);
return;
} else {
uType = 'admin';
trip = hash(password + core.config.tripSalt);
}
} else if (password) {
trip = hash(password + core.config.tripSalt);
}
return;
} else {
uType = 'admin';
trip = hash(password + core.config.tripSalt);
}
} 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';
}
// TODO: Disallow moderator impersonation
for (let mod of core.config.mods) {
if (trip === mod.trip)
uType = 'mod';
}
// Announce the new user
server.broadcast({
cmd: 'onlineAdd',
nick: nick,
trip: trip || 'null'
}, { channel: channel });
// Announce the new user
server.broadcast({
cmd: 'onlineAdd',
nick: nick,
trip: trip || 'null'
}, { channel: channel });
socket.uType = uType;
socket.nick = nick;
socket.channel = channel;
if (trip !== null) socket.trip = trip;
socket.uType = uType;
socket.nick = nick;
socket.channel = channel;
if (trip !== null) socket.trip = trip;
// Reply with online user list
let nicks = [];
for (let client of server.clients) {
if (client.channel === channel) {
nicks.push(client.nick);
}
}
// Reply with online user list
let nicks = [];
for (let client of server.clients) {
if (client.channel === channel) {
nicks.push(client.nick);
}
}
server.reply({
cmd: 'onlineSet',
nicks: nicks
}, socket);
server.reply({
cmd: 'onlineSet',
nicks: nicks
}, socket);
core.managers.stats.increment('users-joined');
core.managers.stats.increment('users-joined');
};
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'
name: 'join',
usage: 'join {channel} {nick}',
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
const createReply = (echoInput) => {
if (echoInput.length > 100)
echoInput = 'HOW ABOUT NO?';
if (echoInput.length > 100)
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)
// be sure it's asyn too
exports.run = async (core, server, socket, data) => {
server.reply({
cmd: 'info',
text: `SHOWCASE MODULE: ${core.showcase} - ${this.createReply(data.echo)}`
}, socket);
server.reply({
cmd: 'info',
text: `SHOWCASE MODULE: ${core.showcase} - ${this.createReply(data.echo)}`
}, socket);
};
@ -29,9 +29,9 @@ exports.run = async (core, server, socket, data) => {
// 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
exports.init = (core) => {
if (typeof core.showcase === 'undefined') {
core.showcase = 'init is a handy place to put global data by assigning it to `core`';
}
if (typeof core.showcase === 'undefined') {
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
@ -39,8 +39,8 @@ exports.requiredData = ['echo'];
// optional parameters are marked, all others are required
exports.info = {
name: 'showcase', // actual command name
aliases: ['templateModule'], // optional, an array of other names this module can be executed by
usage: 'showcase {echo}', // used for help output
description: 'Simple command module template & info' // used for help output
name: 'showcase', // actual command name
aliases: ['templateModule'], // optional, an array of other names this module can be executed by
usage: 'showcase {echo}', // 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 formatTime = (time) => {
let seconds = time[0] + time[1] / 1e9;
let seconds = time[0] + time[1] / 1e9;
let minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
let minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
let hours = Math.floor(minutes / 60);
minutes = minutes % 60;
return `${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`;
let hours = Math.floor(minutes / 60);
minutes = minutes % 60;
return `${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`;
};
exports.run = async (core, server, socket, data) => {
let ips = {};
let channels = {};
for (let client of server.clients) {
if (client.channel) {
channels[client.channel] = true;
ips[client.remoteAddress] = true;
}
}
let ips = {};
let channels = {};
for (let client of server.clients) {
if (client.channel) {
channels[client.channel] = true;
ips[client.remoteAddress] = true;
}
}
let uniqueClientCount = Object.keys(ips).length;
let uniqueChannels = Object.keys(channels).length;
let uniqueClientCount = Object.keys(ips).length;
let uniqueChannels = Object.keys(channels).length;
ips = null;
channels = null;
ips = null;
channels = null;
server.reply({
cmd: 'info',
text: stripIndents`current-connections: ${uniqueClientCount}
current-channels: ${uniqueChannels}
users-joined: ${(core.managers.stats.get('users-joined') || 0)}
invites-sent: ${(core.managers.stats.get('invites-sent') || 0)}
messages-sent: ${(core.managers.stats.get('messages-sent') || 0)}
users-banned: ${(core.managers.stats.get('users-banned') || 0)}
stats-requested: ${(core.managers.stats.get('stats-requested') || 0)}
server-uptime: ${formatTime(process.hrtime(core.managers.stats.get('start-time')))}`
}, socket);
server.reply({
cmd: 'info',
text: stripIndents`current-connections: ${uniqueClientCount}
current-channels: ${uniqueChannels}
users-joined: ${(core.managers.stats.get('users-joined') || 0)}
invites-sent: ${(core.managers.stats.get('invites-sent') || 0)}
messages-sent: ${(core.managers.stats.get('messages-sent') || 0)}
users-banned: ${(core.managers.stats.get('users-banned') || 0)}
stats-requested: ${(core.managers.stats.get('stats-requested') || 0)}
server-uptime: ${formatTime(process.hrtime(core.managers.stats.get('start-time')))}`
}, socket);
core.managers.stats.increment('stats-requested');
core.managers.stats.increment('stats-requested');
};
exports.info = {
name: 'stats',
usage: 'stats',
description: 'Sends back current server stats to the calling client'
name: 'stats',
usage: 'stats',
description: 'Sends back current server stats to the calling client'
};

View File

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

View File

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

View File

@ -5,30 +5,30 @@
'use strict';
exports.run = async (core, server, socket, data) => {
if (socket.uType == 'user') {
// ignore if not mod or admin
return;
}
if (socket.uType == 'user') {
// ignore if not mod or admin
return;
}
let ip = String(data.ip);
let nick = String(data.nick); // for future upgrade
let ip = String(data.ip);
let nick = String(data.nick); // for future upgrade
// TODO: remove ip from ratelimiter
// POLICE.pardon(ip)
console.log(`${socket.nick} [${socket.trip}] unbanned ${/*nick || */ip} in ${socket.channel}`);
// TODO: remove ip from ratelimiter
// POLICE.pardon(ip)
console.log(`${socket.nick} [${socket.trip}] unbanned ${/*nick || */ip} in ${socket.channel}`);
server.reply({
cmd: 'info',
text: `Unbanned ${/*nick || */ip}`
}, socket);
server.reply({
cmd: 'info',
text: `Unbanned ${/*nick || */ip}`
}, socket);
core.managers.stats.decrement('users-banned');
core.managers.stats.decrement('users-banned');
};
exports.requiredData = ['ip'];
exports.info = {
name: 'unban',
usage: 'unban {ip}',
description: 'Removes target ip from the ratelimiter'
name: 'unban',
usage: 'unban {ip}',
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
* denies command execution based on comparison with `threshold`
*
* Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
*
*/
* Tracks frequency of occurances based on `id` (remote address), then allows or
* denies command execution based on comparison with `threshold`
*
* Version: v2.0.0
* Developer: Marzavec ( https://github.com/marzavec )
* License: WTFPL ( http://www.wtfpl.net/txt/copying/ )
*
*/
'use strict';
class Police {
/**
* Create a ratelimiter instance.
*/
constructor () {
this._records = {};
this._halflife = 30000; // ms
this._threshold = 25;
}
/**
* Create a ratelimiter instance.
*/
constructor () {
this._records = {};
this._halflife = 30000; // ms
this._threshold = 25;
}
/**
* Finds current score by `id`
*
* @param {String} id target id / address
* @public
*
* @memberof Police
*/
search (id) {
let record = this._records[id];
/**
* Finds current score by `id`
*
* @param {String} id target id / address
* @public
*
* @memberof Police
*/
search (id) {
let record = this._records[id];
if (!record) {
record = this._records[id] = {
time: Date.now(),
score: 0
}
}
if (!record) {
record = this._records[id] = {
time: Date.now(),
score: 0
}
}
return record;
}
return record;
}
/**
* Adjusts the current ratelimit score by `deltaScore`
*
* @param {String} id target id / address
* @param {Number} deltaScore amount to adjust current score by
* @public
*
* @memberof Police
*/
frisk (id, deltaScore) {
let record = this.search(id);
/**
* Adjusts the current ratelimit score by `deltaScore`
*
* @param {String} id target id / address
* @param {Number} deltaScore amount to adjust current score by
* @public
*
* @memberof Police
*/
frisk (id, deltaScore) {
let record = this.search(id);
if (record.arrested) {
return true;
}
if (record.arrested) {
return true;
}
record.score *= Math.pow(2, -(Date.now() - record.time ) / this._halflife);
record.score += deltaScore;
record.time = Date.now();
record.score *= Math.pow(2, -(Date.now() - record.time ) / this._halflife);
record.score += deltaScore;
record.time = Date.now();
if (record.score >= this._threshold) {
return true;
}
if (record.score >= this._threshold) {
return true;
}
return false;
}
return false;
}
/**
* Statically set server to no longer accept traffic from `id`
*
* @param {String} id target id / address
* @public
*
* @memberof Police
*/
arrest (id) {
var record = this.search(id);
/**
* Statically set server to no longer accept traffic from `id`
*
* @param {String} id target id / address
* @public
*
* @memberof Police
*/
arrest (id) {
var record = this.search(id);
if (record) {
record.arrested = true;
}
}
if (record) {
record.arrested = true;
}
}
/**
* Remove statically assigned limit from `id`
*
* @param {String} id target id / address
* @public
*
* @memberof Police
*/
pardon (id) {
var record = this.search(id);
/**
* Remove statically assigned limit from `id`
*
* @param {String} id target id / address
* @public
*
* @memberof Police
*/
pardon (id) {
var record = this.search(id);
if (record) {
record.arrested = false;
}
}
if (record) {
record.arrested = false;
}
}
}
module.exports = Police;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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