mirror of
https://github.com/hack-chat/main.git
synced 2024-03-22 13:20:33 +08:00
Added hooks, modules and cleaned up code
This commit is contained in:
parent
c06681803b
commit
c719020e17
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.1.0] - 2018-09-29
|
||||
### Added
|
||||
- Module hook framework, isolating modules and making them truly drop-to-install
|
||||
- `./server/src/commands/core/whisper.js` module to send in-channel private messages, `/whisper` hook
|
||||
- `muzzle` and `mute` aliases to `./server/src/commands/mod/dumb.js`
|
||||
- `unmuzzle` and `unmute` aliases to `./server/src/commands/mod/speak.js`
|
||||
- `./server/src/commands/admin/removemod.js` module to remove mods
|
||||
- `./server/src/commands/mod/unbanall.js` module to clear all bans and ratelimiting
|
||||
|
||||
### Changed
|
||||
- Further code cleanup on all modules
|
||||
- Adjusted `ipSalt` entropy
|
||||
- `./server/src/commands/core/help.js` output is now helpful, added `/help` hook
|
||||
- `./server/src/commands/core/chat.js` added `/myhash` and `/me` hooks
|
||||
- `./server/src/commands/core/morestats.js` added `/stats` hook
|
||||
|
||||
## [2.0.3] - 2018-06-03
|
||||
### Added
|
||||
- `./server/src/commands/mod/dumb.js` module for server-wide shadow muting
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hack.chat-v2",
|
||||
"version": "2.0.3",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hack.chat-v2",
|
||||
"version": "2.0.3",
|
||||
"version": "2.1.0",
|
||||
"description": "a minimal distraction free chat application",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
|
|
2
server/package-lock.json
generated
2
server/package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hack.chat-v2",
|
||||
"version": "2.0.3",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "hack.chat-v2",
|
||||
"version": "2.0.3",
|
||||
"version": "2.1.0",
|
||||
"description": "a minimal distraction free chat application",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
|
|
|
@ -2,23 +2,24 @@
|
|||
Description: Adds the target trip to the mod list then elevates the uType
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin
|
||||
if (socket.uType != 'admin') {
|
||||
server._police.frisk(socket.remoteAddress, 20);
|
||||
|
||||
return;
|
||||
return server._police.frisk(socket.remoteAddress, 20);
|
||||
}
|
||||
|
||||
// add new trip to config
|
||||
core.config.mods.push({ trip: data.trip }); // purposely not using `config.set()` to avoid auto-save
|
||||
|
||||
// upgarde existing connections & notify user
|
||||
// find targets current connections
|
||||
let newMod = server.findSockets({ trip: data.trip });
|
||||
if (newMod.length !== 0) {
|
||||
for (let i = 0, l = newMod.length; i < l; i++) {
|
||||
// upgrade privilages
|
||||
newMod[i].uType = 'mod';
|
||||
|
||||
// inform new mod
|
||||
server.send({
|
||||
cmd: 'info',
|
||||
text: 'You are now a mod.'
|
||||
|
@ -29,20 +30,21 @@ exports.run = async (core, server, socket, data) => {
|
|||
// return success message
|
||||
server.reply({
|
||||
cmd: 'info',
|
||||
text: `Added mod trip: ${data.trip}`
|
||||
text: `Added mod trip: ${data.trip}, remember to run 'saveconfig' to make it permanent`
|
||||
}, socket);
|
||||
|
||||
// notify all mods
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `Added mod trip: ${data.trip}`
|
||||
text: `Added mod: ${data.trip}`
|
||||
}, { uType: 'mod' });
|
||||
};
|
||||
|
||||
// module meta
|
||||
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'
|
||||
description: 'Adds target trip to the config as a mod and upgrades the socket type',
|
||||
usage: `
|
||||
API: { cmd: 'addmod', trip: '<target trip>' }`
|
||||
};
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
Description: Outputs all current channels and their user nicks
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin
|
||||
if (socket.uType != 'admin') {
|
||||
server._police.frisk(socket.remoteAddress, 20);
|
||||
|
||||
return;
|
||||
return server._police.frisk(socket.remoteAddress, 20);
|
||||
}
|
||||
|
||||
// find all users currently in a channel
|
||||
|
@ -37,7 +36,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
}, socket);
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'listusers',
|
||||
description: 'Outputs all current channels and sockets in those channels'
|
||||
description: 'Outputs all current channels and sockets in those channels',
|
||||
usage: `
|
||||
API: { cmd: 'listusers' }`
|
||||
};
|
||||
|
|
|
@ -2,18 +2,21 @@
|
|||
Description: Clears and resets the command modules, outputting any errors
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin
|
||||
if (socket.uType != 'admin') {
|
||||
server._police.frisk(socket.remoteAddress, 20);
|
||||
|
||||
return;
|
||||
return server._police.frisk(socket.remoteAddress, 20);
|
||||
}
|
||||
|
||||
// do command reloads and store results
|
||||
let loadResult = core.managers.dynamicImports.reloadDirCache('src/commands');
|
||||
loadResult += core.commands.loadCommands();
|
||||
|
||||
// clear and rebuild all module hooks
|
||||
server.clearHooks();
|
||||
core.commands.initCommandHooks(server);
|
||||
|
||||
// build reply based on reload results
|
||||
if (loadResult == '') {
|
||||
loadResult = `Loaded ${core.commands._commands.length} commands, 0 errors`;
|
||||
|
@ -34,7 +37,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
}, { uType: 'mod' });
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: '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',
|
||||
usage: `
|
||||
API: { cmd: 'reload' }`
|
||||
};
|
||||
|
|
50
server/src/commands/admin/removemod.js
Normal file
50
server/src/commands/admin/removemod.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
Description: Removes target trip from the config as a mod and downgrades the socket type
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin
|
||||
if (socket.uType != 'admin') {
|
||||
return server._police.frisk(socket.remoteAddress, 20);
|
||||
}
|
||||
|
||||
// remove trip from config
|
||||
core.config.mods = core.config.mods.filter(mod => mod.trip !== data.trip);
|
||||
|
||||
// find targets current connections
|
||||
let targetMod = server.findSockets({ trip: data.trip });
|
||||
if (newMod.length !== 0) {
|
||||
for (let i = 0, l = newMod.length; i < l; i++) {
|
||||
// downgrade privilages
|
||||
targetMod[i].uType = 'user';
|
||||
|
||||
// inform ex-mod
|
||||
server.send({
|
||||
cmd: 'info',
|
||||
text: 'You are now a user.'
|
||||
}, targetMod[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// return success message
|
||||
server.reply({
|
||||
cmd: 'info',
|
||||
text: `Removed mod trip: ${data.trip}, remember to run 'saveconfig' to make it permanent`
|
||||
}, socket);
|
||||
|
||||
// notify all mods
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `Removed mod: ${data.trip}`
|
||||
}, { uType: 'mod' });
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['trip'];
|
||||
exports.info = {
|
||||
name: 'removemod',
|
||||
description: 'Removes target trip from the config as a mod and downgrades the socket type',
|
||||
usage: `
|
||||
API: { cmd: 'removemod', trip: '<target trip>' }`
|
||||
};
|
|
@ -2,22 +2,19 @@
|
|||
Description: Writes the current config to disk
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin
|
||||
if (socket.uType != 'admin') {
|
||||
server._police.frisk(socket.remoteAddress, 20);
|
||||
|
||||
return;
|
||||
return server._police.frisk(socket.remoteAddress, 20);
|
||||
}
|
||||
|
||||
// attempt save, notify of failure
|
||||
if (!core.managers.config.save()) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Failed to save config, check logs.'
|
||||
}, client);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// return success message
|
||||
|
@ -33,7 +30,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
}, { uType: 'mod' });
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'saveconfig',
|
||||
description: 'Writes the current config to disk'
|
||||
description: 'Writes the current config to disk',
|
||||
usage: `
|
||||
API: { cmd: 'saveconfig' }`
|
||||
};
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
Description: Emmits a server-wide message as `info`
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin
|
||||
if (socket.uType != 'admin') {
|
||||
server._police.frisk(socket.remoteAddress, 20);
|
||||
|
||||
return;
|
||||
return server._police.frisk(socket.remoteAddress, 20);
|
||||
}
|
||||
|
||||
// send text to all channels
|
||||
|
@ -17,10 +16,11 @@ exports.run = async (core, server, socket, data) => {
|
|||
}, {});
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['text'];
|
||||
|
||||
exports.info = {
|
||||
name: 'shout',
|
||||
usage: 'shout {text}',
|
||||
description: 'Displays passed text to every client connected'
|
||||
description: 'Displays passed text to every client connected',
|
||||
usage: `
|
||||
API: { cmd: 'shout', text: '<shout text>' }`
|
||||
};
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
/*
|
||||
Description: Generates a semi-unique channel name then broadcasts it to each client
|
||||
Description: Allows calling client to change their current nickname
|
||||
*/
|
||||
|
||||
// module support functions
|
||||
const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick);
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
if (server._police.frisk(socket.remoteAddress, 6)) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are changing nicknames too fast. Wait a moment before trying again.'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// verify user data is string
|
||||
|
@ -22,12 +22,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
// make sure requested nickname meets standards
|
||||
let newNick = data.nick.trim();
|
||||
if (!verifyNickname(newNick)) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname must consist of up to 24 letters, numbers, and underscores'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// prevent admin impersonation
|
||||
|
@ -35,12 +33,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
if (newNick.toLowerCase() == core.config.adminName.toLowerCase()) {
|
||||
server._police.frisk(socket.remoteAddress, 4);
|
||||
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Gtfo'
|
||||
text: 'You are not the admin, liar!'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// find any sockets that have the same nickname
|
||||
|
@ -52,12 +48,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
// return error if found
|
||||
if (userExists.length > 0) {
|
||||
// That nickname is already in that channel
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname taken'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// build join and leave notices
|
||||
|
@ -65,6 +59,7 @@ exports.run = async (core, server, socket, data) => {
|
|||
cmd: 'onlineRemove',
|
||||
nick: socket.nick
|
||||
};
|
||||
|
||||
let joinNotice = {
|
||||
cmd: 'onlineAdd',
|
||||
nick: newNick,
|
||||
|
@ -86,10 +81,11 @@ exports.run = async (core, server, socket, data) => {
|
|||
socket.nick = newNick;
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['nick'];
|
||||
|
||||
exports.info = {
|
||||
name: 'changenick',
|
||||
usage: 'changenick {nick}',
|
||||
description: 'This will change your current connections nickname'
|
||||
description: 'This will change your current connections nickname',
|
||||
usage: `
|
||||
API: { cmd: 'changenick', nick: '<new nickname>' }`
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Description: Rebroadcasts any `text` to all clients in a `channel`
|
||||
*/
|
||||
|
||||
// module support functions
|
||||
const parseText = (text) => {
|
||||
// verifies user input is text
|
||||
if (typeof text !== 'string') {
|
||||
|
@ -16,25 +17,23 @@ const parseText = (text) => {
|
|||
return text;
|
||||
};
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// check user input
|
||||
let text = parseText(data.text);
|
||||
|
||||
if (!text) {
|
||||
// lets not send objects or empty text, yea?
|
||||
server._police.frisk(socket.remoteAddress, 6);
|
||||
|
||||
return;
|
||||
return server._police.frisk(socket.remoteAddress, 13);
|
||||
}
|
||||
|
||||
// check for spam
|
||||
let score = text.length / 83 / 4;
|
||||
if (server._police.frisk(socket.remoteAddress, score)) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// build chat payload
|
||||
|
@ -54,26 +53,77 @@ exports.run = async (core, server, socket, data) => {
|
|||
payload.trip = socket.trip;
|
||||
}
|
||||
|
||||
// check if the users connection is muted
|
||||
// TODO: Add a more contained way for modules to interact, event hooks or something?
|
||||
if(core.muzzledHashes && core.muzzledHashes[socket.hash]){
|
||||
server.broadcast( payload, { channel: socket.channel, hash: socket.hash });
|
||||
if(core.muzzledHashes[socket.hash].allies){
|
||||
server.broadcast( payload, { channel: socket.channel, nick: core.muzzledHashes[socket.hash].allies });
|
||||
}
|
||||
} else {
|
||||
//else send it to everyone
|
||||
server.broadcast( payload, { channel: socket.channel});
|
||||
}
|
||||
// broadcast to channel peers
|
||||
server.broadcast( payload, { channel: socket.channel});
|
||||
|
||||
// stats are fun
|
||||
core.managers.stats.increment('messages-sent');
|
||||
};
|
||||
|
||||
exports.requiredData = ['text'];
|
||||
// module hook functions
|
||||
exports.initHooks = (server) => {
|
||||
server.registerHook('in', 'chat', this.commandCheckIn);
|
||||
server.registerHook('out', 'chat', this.commandCheckOut);
|
||||
};
|
||||
|
||||
// checks for miscellaneous '/' based commands
|
||||
exports.commandCheckIn = (core, server, socket, payload) => {
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/myhash')) {
|
||||
server.reply({
|
||||
cmd: 'info',
|
||||
text: `Your hash: ${socket.hash}`
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// checks for miscellaneous '/' based commands
|
||||
exports.commandCheckOut = (core, server, socket, payload) => {
|
||||
if (!payload.text.startsWith('/')) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('//me ')) {
|
||||
payload.text = payload.text.substr(1, payload.text.length);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
// TODO: make emotes their own module #lazydev
|
||||
if (payload.text.startsWith('/me ')) {
|
||||
let emote = payload.text.substr(4);
|
||||
if (emote.trim() === '') {
|
||||
emote = 'fails at life';
|
||||
}
|
||||
|
||||
let newPayload = {
|
||||
cmd: 'info',
|
||||
type: 'emote',
|
||||
text: `@${payload.nick} ${emote}`
|
||||
};
|
||||
|
||||
return newPayload;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['text'];
|
||||
exports.info = {
|
||||
name: 'chat',
|
||||
usage: 'chat {text}',
|
||||
description: 'Broadcasts passed `text` field to the calling users channel'
|
||||
description: 'Broadcasts passed `text` field to the calling users channel',
|
||||
usage: `
|
||||
API: { cmd: 'chat', text: '<text to send>' }
|
||||
Text: Uuuuhm. Just kind type in that little box at the bottom and hit enter.\n
|
||||
Bonus super secret hidden commands:
|
||||
/me <emote>
|
||||
/myhash`
|
||||
};
|
||||
|
|
|
@ -2,41 +2,50 @@
|
|||
Description: Outputs the current command module list or command categories
|
||||
*/
|
||||
|
||||
// module support functions
|
||||
const stripIndents = require('common-tags').stripIndents;
|
||||
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// TODO: this module needs to be clean up badly :(
|
||||
// module main
|
||||
exports.run = async (core, server, socket, payload) => {
|
||||
// check for spam
|
||||
if (server._police.frisk(socket.remoteAddress, 2)) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.'
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// verify passed arguments
|
||||
let typeDt = typeof data.type;
|
||||
let catDt = typeof data.category;
|
||||
let cmdDt = typeof data.command;
|
||||
if (typeDt !== 'undefined' && typeDt !== 'string' ) {
|
||||
return;
|
||||
} else if (catDt !== 'undefined' && catDt !== 'string' ) {
|
||||
return;
|
||||
} else if (cmdDt !== 'undefined' && cmdDt !== 'string' ) {
|
||||
// verify user input
|
||||
if (typeof payload.command !== 'undefined' && typeof payload.command !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
// set default reply
|
||||
let reply = stripIndents`Help usage:
|
||||
Show all categories -> { cmd: 'help', type: 'categories' }
|
||||
Show all commands in category -> { cmd: 'help', category: '<category name>' }
|
||||
Show specific command -> { cmd: 'help', command: '<command name>' }`;
|
||||
let reply = '';
|
||||
if (typeof payload.command === 'undefined') {
|
||||
reply = stripIndents`Listing all current commands. For specific help on certain commands, use either:
|
||||
Text: /help <command name>
|
||||
API: {cmd: 'help', command: '<command name>'}`;
|
||||
reply += '\n\n-------------------------------------\n\n';
|
||||
|
||||
// change reply based on query
|
||||
if (typeDt !== 'undefined') {
|
||||
let categories = core.commands.categories().sort();
|
||||
reply = `Command Categories:\n${categories.map(c => `- ${c.replace('../src/commands/', '')}`).join('\n')}`;
|
||||
} else if (catDt !== 'undefined') {
|
||||
let catCommands = core.commands.all('../src/commands/' + data.category).sort((a, b) => a.info.name.localeCompare(b.info.name));
|
||||
reply = `${data.category} commands:\n${catCommands.map(c => `- ${c.info.name}`).join('\n')}`;
|
||||
} else if (cmdDt !== 'undefined') {
|
||||
let command = core.commands.get(data.command);
|
||||
reply = stripIndents`
|
||||
Usage: ${command.info.usage || command.info.name}
|
||||
Description: ${command.info.description || '¯\_(ツ)_/¯'}`;
|
||||
for (let i = 0, j = categories.length; i < j; i++) {
|
||||
reply += `${categories[i].replace('../src/commands/', '').replace(/^\w/, c => c.toUpperCase())} Commands:\n`;
|
||||
let catCommands = core.commands.all(categories[i]).sort((a, b) => a.info.name.localeCompare(b.info.name));
|
||||
reply += ` ${catCommands.map(c => `${c.info.name}`).join(', ')}\n\n`;
|
||||
}
|
||||
} else {
|
||||
let command = core.commands.get(payload.command);
|
||||
|
||||
if (typeof command === 'undefined') {
|
||||
reply = 'Unknown command';
|
||||
} else {
|
||||
reply = stripIndents`Name: ${command.info.name}
|
||||
Aliases: ${typeof command.info.aliases !== 'undefined' ? command.info.aliases.join(', ') : 'None'}
|
||||
Category: ${command.info.category.replace('../src/commands/', '').replace(/^\w/, c => c.toUpperCase())}
|
||||
Required Parameters: ${command.requiredData || 'None'}\n
|
||||
Description: ${command.info.description || '¯\_(ツ)_/¯'}\n
|
||||
Usage: ${command.info.usage || command.info.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
// output reply
|
||||
|
@ -46,8 +55,36 @@ exports.run = async (core, server, socket, data) => {
|
|||
}, socket);
|
||||
};
|
||||
|
||||
// module hook functions
|
||||
exports.initHooks = (server) => {
|
||||
server.registerHook('in', 'chat', this.helpCheck);
|
||||
};
|
||||
|
||||
// hooks chat commands checking for /whisper
|
||||
exports.helpCheck = (core, server, socket, payload) => {
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/help')) {
|
||||
let input = payload.text.substr(1, payload.text.length).split(' ', 2);
|
||||
|
||||
this.run(core, server, socket, {
|
||||
cmd: input[0],
|
||||
command: input[1]
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'help',
|
||||
usage: 'help ([ type:categories] | [category:<category name> | command:<command name> ])',
|
||||
description: 'Outputs information about the servers current protocol'
|
||||
description: 'Outputs information about the servers current protocol',
|
||||
usage: `
|
||||
API: { cmd: 'help', command: '<optional command name>' }
|
||||
Text: /help <optional command name>`
|
||||
};
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
Description: Generates a semi-unique channel name then broadcasts it to each client
|
||||
*/
|
||||
|
||||
// module support functions
|
||||
const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick);
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// check for spam
|
||||
if (server._police.frisk(socket.remoteAddress, 2)) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are sending invites too fast. Wait a moment before trying again.'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// verify user input
|
||||
|
@ -34,16 +34,15 @@ exports.run = async (core, server, socket, data) => {
|
|||
invite: channel,
|
||||
text: `${socket.nick} invited you to ?${channel}`
|
||||
};
|
||||
|
||||
let inviteSent = server.broadcast( payload, { channel: socket.channel, nick: data.nick });
|
||||
|
||||
// server indicates the user was not found
|
||||
if (!inviteSent) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in channel'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// reply with common channel
|
||||
|
@ -56,10 +55,11 @@ exports.run = async (core, server, socket, data) => {
|
|||
core.managers.stats.increment('invites-sent');
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['nick'];
|
||||
|
||||
exports.info = {
|
||||
name: 'invite',
|
||||
usage: 'invite {nick}',
|
||||
description: 'Generates a unique (more or less) room name and passes it to two clients'
|
||||
description: 'Generates a unique (more or less) room name and passes it to two clients',
|
||||
usage: `
|
||||
API: { cmd: 'invite', nick: '<target nickname>' }`
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Description: Initial entry point, applies `channel` and `nick` to the calling socket
|
||||
*/
|
||||
|
||||
// module support functions
|
||||
const crypto = require('crypto');
|
||||
|
||||
const hash = (password) => {
|
||||
|
@ -12,15 +13,54 @@ const hash = (password) => {
|
|||
|
||||
const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick);
|
||||
|
||||
// exposed "login" function to allow hooks to verify user join events
|
||||
// returns object containing user info or string if error
|
||||
exports.parseNickname = (core, data) => {
|
||||
let userInfo = {
|
||||
nick: '',
|
||||
uType: 'user',
|
||||
trip: null,
|
||||
};
|
||||
|
||||
// seperate nick from password
|
||||
let nickArray = data.nick.split('#', 2);
|
||||
userInfo.nick = nickArray[0].trim();
|
||||
|
||||
if (!verifyNickname(userInfo.nick)) {
|
||||
// return error as string
|
||||
return 'Nickname must consist of up to 24 letters, numbers, and underscores';
|
||||
}
|
||||
|
||||
let password = nickArray[1];
|
||||
if (userInfo.nick.toLowerCase() == core.config.adminName.toLowerCase()) {
|
||||
if (password !== core.config.adminPass) {
|
||||
return 'You are not the admin, liar!';
|
||||
} else {
|
||||
userInfo.uType = 'admin';
|
||||
userInfo.trip = 'Admin';
|
||||
}
|
||||
} else if (password) {
|
||||
userInfo.trip = hash(password + core.config.tripSalt);
|
||||
}
|
||||
|
||||
// TODO: disallow moderator impersonation
|
||||
for (let mod of core.config.mods) {
|
||||
if (userInfo.trip === mod.trip) {
|
||||
userInfo.uType = 'mod';
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
};
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// check for spam
|
||||
if (server._police.frisk(socket.remoteAddress, 3)) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are joining channels too fast. Wait a moment and try again.'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// calling socket already in a channel
|
||||
|
@ -39,75 +79,39 @@ exports.run = async (core, server, socket, data) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// process nickname
|
||||
let nick = data.nick;
|
||||
let nickArray = nick.split('#', 2);
|
||||
nick = nickArray[0].trim();
|
||||
|
||||
if (!verifyNickname(nick)) {
|
||||
server.reply({
|
||||
let userInfo = this.parseNickname(core, data);
|
||||
if (typeof userInfo === 'string') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname must consist of up to 24 letters, numbers, and underscores'
|
||||
text: userInfo
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the nickname already exists in the channel
|
||||
let userExists = server.findSockets({
|
||||
channel: data.channel,
|
||||
nick: (targetNick) => targetNick.toLowerCase() === nick.toLowerCase()
|
||||
nick: (targetNick) => targetNick.toLowerCase() === userInfo.nick.toLowerCase()
|
||||
});
|
||||
|
||||
if (userExists.length > 0) {
|
||||
// that nickname is already in that channel
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname taken'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: should we check for mod status first to prevent overwriting of admin status somehow? Meh, w/e, cba.
|
||||
let uType = 'user';
|
||||
let trip = null;
|
||||
let password = nickArray[1];
|
||||
if (nick.toLowerCase() == core.config.adminName.toLowerCase()) {
|
||||
if (password != core.config.adminPass) {
|
||||
server._police.frisk(socket.remoteAddress, 4);
|
||||
|
||||
server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Gtfo'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
} else {
|
||||
uType = 'admin';
|
||||
trip = 'Admin';
|
||||
}
|
||||
} else if (password) {
|
||||
trip = hash(password + core.config.tripSalt);
|
||||
}
|
||||
|
||||
// TODO: disallow moderator impersonation
|
||||
for (let mod of core.config.mods) {
|
||||
if (trip === mod.trip) {
|
||||
uType = 'mod';
|
||||
}
|
||||
}
|
||||
userInfo.userHash = server.getSocketHash(socket);
|
||||
|
||||
// prepare to notify channel peers
|
||||
let newPeerList = server.findSockets({ channel: data.channel });
|
||||
let userHash = server.getSocketHash(socket);
|
||||
let nicks = [];
|
||||
|
||||
let joinAnnouncement = {
|
||||
cmd: 'onlineAdd',
|
||||
nick: nick,
|
||||
trip: trip || 'null',
|
||||
hash: userHash
|
||||
nick: userInfo.nick,
|
||||
trip: userInfo.trip || 'null',
|
||||
hash: userInfo.userHash
|
||||
};
|
||||
|
||||
// send join announcement and prep online set
|
||||
|
@ -117,11 +121,11 @@ exports.run = async (core, server, socket, data) => {
|
|||
}
|
||||
|
||||
// store user info
|
||||
socket.uType = uType;
|
||||
socket.nick = nick;
|
||||
socket.channel = channel;
|
||||
socket.hash = userHash;
|
||||
if (trip !== null) socket.trip = trip;
|
||||
socket.uType = userInfo.uType;
|
||||
socket.nick = userInfo.nick;
|
||||
socket.channel = data.channel;
|
||||
socket.hash = userInfo.userHash;
|
||||
if (userInfo.trip !== null) socket.trip = userInfo.trip;
|
||||
|
||||
nicks.push(socket.nick);
|
||||
|
||||
|
@ -135,10 +139,11 @@ exports.run = async (core, server, socket, data) => {
|
|||
core.managers.stats.increment('users-joined');
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['channel', 'nick'];
|
||||
|
||||
exports.info = {
|
||||
name: 'join',
|
||||
usage: 'join {channel} {nick}',
|
||||
description: 'Place calling socket into target channel with target nick & broadcast event to channel'
|
||||
description: 'Place calling socket into target channel with target nick & broadcast event to channel',
|
||||
usage: `
|
||||
API: { cmd: 'join', nick: '<your nickname>', channel: '<target channel>' }`
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Description: Outputs more info than the legacy stats command
|
||||
*/
|
||||
|
||||
// module support functions
|
||||
const stripIndents = require('common-tags').stripIndents;
|
||||
|
||||
const formatTime = (time) => {
|
||||
|
@ -19,6 +20,7 @@ const formatTime = (time) => {
|
|||
return `${days.toFixed(0)}d ${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`;
|
||||
};
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// gather connection and channel count
|
||||
let ips = {};
|
||||
|
@ -54,7 +56,33 @@ exports.run = async (core, server, socket, data) => {
|
|||
core.managers.stats.increment('stats-requested');
|
||||
};
|
||||
|
||||
// module hook functions
|
||||
exports.initHooks = (server) => {
|
||||
server.registerHook('in', 'chat', this.statsCheck);
|
||||
};
|
||||
|
||||
// hooks chat commands checking for /stats
|
||||
exports.statsCheck = (core, server, socket, payload) => {
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/stats')) {
|
||||
this.run(core, server, socket, {
|
||||
cmd: 'morestats'
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'morestats',
|
||||
description: 'Sends back current server stats to the calling client'
|
||||
description: 'Sends back current server stats to the calling client',
|
||||
usage: `
|
||||
API: { cmd: 'morestats' }
|
||||
Text: /stats`
|
||||
};
|
||||
|
|
|
@ -2,15 +2,14 @@
|
|||
Description: Changes the current channel of the calling socket
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// check for spam
|
||||
if (server._police.frisk(socket.remoteAddress, 6)) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are changing channels too fast. Wait a moment before trying again.'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check user input
|
||||
|
@ -81,10 +80,11 @@ exports.run = async (core, server, socket, data) => {
|
|||
socket.channel = data.channel;
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['channel'];
|
||||
|
||||
exports.info = {
|
||||
name: 'move',
|
||||
usage: 'move {channel}',
|
||||
description: 'This will change the current channel to the new one provided'
|
||||
description: 'This will change your current channel to the new one provided',
|
||||
usage: `
|
||||
API: { cmd: 'move', channel: '<target channel>' }`
|
||||
};
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
Description: This module is only in place to supress error notices legacy sources may get
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
return;
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'ping',
|
||||
description: 'This module is only in place to supress error notices legacy sources may get'
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Description: Legacy stats output, kept for compatibility, outputs user and channel count
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// gather connection and channel count
|
||||
let ips = {};
|
||||
|
@ -29,7 +30,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
core.managers.stats.increment('stats-requested');
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'stats',
|
||||
description: 'Sends back legacy server stats to the calling client'
|
||||
description: 'Sends back legacy server stats to the calling client',
|
||||
usage: `
|
||||
API: { cmd: 'stats' }`
|
||||
};
|
||||
|
|
110
server/src/commands/core/whisper.js
Normal file
110
server/src/commands/core/whisper.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Description: Display text on targets screen that only they can see
|
||||
*/
|
||||
|
||||
// module support functions
|
||||
const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick);
|
||||
|
||||
const parseText = (text) => {
|
||||
// verifies user input is text
|
||||
if (typeof text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// strip newlines from beginning and end
|
||||
text = text.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
|
||||
// replace 3+ newlines with just 2 newlines
|
||||
text = text.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, payload) => {
|
||||
// check user input
|
||||
let text = parseText(payload.text);
|
||||
|
||||
if (!text) {
|
||||
// lets not send objects or empty text, yea?
|
||||
return server._police.frisk(socket.remoteAddress, 13);
|
||||
}
|
||||
|
||||
// check for spam
|
||||
let score = text.length / 83 / 4;
|
||||
if (server._police.frisk(socket.remoteAddress, score)) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.'
|
||||
}, socket);
|
||||
}
|
||||
|
||||
let targetNick = payload.nick;
|
||||
if (!verifyNickname(targetNick)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find target user
|
||||
let targetClient = server.findSockets({ channel: socket.channel, nick: targetNick });
|
||||
|
||||
if (targetClient.length === 0) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in channel'
|
||||
}, socket);
|
||||
}
|
||||
|
||||
targetClient = targetClient[0];
|
||||
|
||||
server.reply({
|
||||
cmd: 'info',
|
||||
type: 'whisper',
|
||||
from: socket.nick,
|
||||
trip: socket.trip || 'null',
|
||||
text: `${socket.nick} whispered: ${text}`
|
||||
}, targetClient);
|
||||
|
||||
server.reply({
|
||||
cmd: 'info',
|
||||
type: 'whisper',
|
||||
text: `You whispered to @${targetNick}: ${text}`
|
||||
}, socket);
|
||||
};
|
||||
|
||||
// module hook functions
|
||||
exports.initHooks = (server) => {
|
||||
server.registerHook('in', 'chat', this.whisperCheck);
|
||||
};
|
||||
|
||||
// hooks chat commands checking for /whisper
|
||||
exports.whisperCheck = (core, server, socket, payload) => {
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/whisper')) {
|
||||
let input = payload.text.split(' ');
|
||||
let target = input[1].replace(/@/g, '');
|
||||
input.splice(0, 2);
|
||||
let whisperText = input.join(' ');
|
||||
|
||||
this.run(core, server, socket, {
|
||||
cmd: 'whisper',
|
||||
nick: target,
|
||||
text: whisperText
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['nick', 'text'];
|
||||
exports.info = {
|
||||
name: 'whisper',
|
||||
description: 'Display text on targets screen that only they can see',
|
||||
usage: `
|
||||
API: { cmd: 'whisper', nick: '<target name>', text: '<text to whisper>' }
|
||||
Text: /whisper <target name> <text to whisper>`
|
||||
};
|
|
@ -3,12 +3,11 @@
|
|||
when a socket connection is closed or lost.
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
if (data.cmdKey !== server._cmdKey) {
|
||||
// internal command attempt by client, increase rate limit chance and ignore
|
||||
server._police.frisk(socket.remoteAddress, 20);
|
||||
|
||||
return;
|
||||
return server._police.frisk(socket.remoteAddress, 20);
|
||||
}
|
||||
|
||||
// send leave notice to client peers
|
||||
|
@ -23,8 +22,8 @@ exports.run = async (core, server, socket, data) => {
|
|||
socket.terminate();
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['cmdKey'];
|
||||
|
||||
exports.info = {
|
||||
name: 'disconnect',
|
||||
usage: 'Internal Use Only',
|
||||
|
|
|
@ -2,20 +2,19 @@
|
|||
Description: Used to relay warnings to clients internally
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
if (data.cmdKey !== server._cmdKey) {
|
||||
// internal command attempt by client, increase rate limit chance and ignore
|
||||
server._police.frisk(socket.remoteAddress, 20);
|
||||
|
||||
return;
|
||||
return server._police.frisk(socket.remoteAddress, 20);
|
||||
}
|
||||
|
||||
// send warning to target socket
|
||||
server.reply({ cmd: 'warn', text: data.text }, socket);
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['cmdKey', 'text'];
|
||||
|
||||
exports.info = {
|
||||
name: 'socketreply',
|
||||
usage: 'Internal Use Only',
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
Description: Adds the target socket's ip to the ratelimiter
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (socket.uType == 'user') {
|
||||
server._police.frisk(socket.remoteAddress, 10);
|
||||
|
||||
return;
|
||||
if (socket.uType === 'user') {
|
||||
return server._police.frisk(socket.remoteAddress, 10);
|
||||
}
|
||||
|
||||
// check user input
|
||||
|
@ -20,24 +19,20 @@ exports.run = async (core, server, socket, data) => {
|
|||
let badClient = server.findSockets({ channel: socket.channel, nick: targetNick });
|
||||
|
||||
if (badClient.length === 0) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in channel'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
badClient = badClient[0];
|
||||
|
||||
// i guess banning mods or admins isn't the best idea?
|
||||
if (badClient.uType !== 'user') {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Cannot ban other mods, how rude'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// commit arrest record
|
||||
|
@ -64,10 +59,11 @@ exports.run = async (core, server, socket, data) => {
|
|||
core.managers.stats.increment('users-banned');
|
||||
};
|
||||
|
||||
// module meta
|
||||
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'
|
||||
description: 'Disconnects the target nickname in the same channel as calling socket & adds to ratelimiter',
|
||||
usage: `
|
||||
API: { cmd: 'ban', nick: '<target nickname>' }`
|
||||
};
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
/*
|
||||
* Description: Make a user (spammer) dumb
|
||||
* Description: Make a user (spammer) dumb (mute)
|
||||
* Author: simple
|
||||
*/
|
||||
|
||||
// module constructor
|
||||
exports.init = (core) => {
|
||||
if (typeof core.muzzledHashes === 'undefined') {
|
||||
core.muzzledHashes = {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (socket.uType == 'user') {
|
||||
server._police.frisk(socket.remoteAddress, 10);
|
||||
|
||||
return;
|
||||
if (socket.uType === 'user') {
|
||||
return server._police.frisk(socket.remoteAddress, 10);
|
||||
}
|
||||
|
||||
// check user input
|
||||
|
@ -24,29 +26,25 @@ exports.run = async (core, server, socket, data) => {
|
|||
let badClient = server.findSockets({ channel: socket.channel, nick: data.nick });
|
||||
|
||||
if (badClient.length === 0) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in channel'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
badClient = badClient[0];
|
||||
|
||||
// likely dont need this, muting mods and admins is fine
|
||||
if (badClient.uType !== 'user') {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'This trick wont work on mods and admin'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// store hash in mute list
|
||||
let record = core.muzzledHashes[badClient.hash] = {
|
||||
dumb:true
|
||||
dumb: true
|
||||
}
|
||||
|
||||
// store allies if needed
|
||||
|
@ -59,12 +57,78 @@ exports.run = async (core, server, socket, data) => {
|
|||
cmd: 'info',
|
||||
text: `${socket.nick} muzzled ${data.nick} in ${socket.channel}, userhash: ${badClient.hash}`
|
||||
}, { uType: 'mod' });
|
||||
}
|
||||
};
|
||||
|
||||
// module hook functions
|
||||
exports.initHooks = (server) => {
|
||||
server.registerHook('in', 'chat', this.chatCheck);
|
||||
server.registerHook('in', 'invite', this.inviteCheck);
|
||||
// TODO: add whisper hook, need hook priorities todo finished first
|
||||
};
|
||||
|
||||
// hook incoming chat commands, shadow-prevent chat if they are muzzled
|
||||
exports.chatCheck = (core, server, socket, payload) => {
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(core.muzzledHashes[socket.hash]){
|
||||
// build fake chat payload
|
||||
mutedPayload = {
|
||||
cmd: 'chat',
|
||||
nick: socket.nick,
|
||||
text: payload.text
|
||||
};
|
||||
|
||||
if (socket.trip) {
|
||||
mutedPayload.trip = socket.trip;
|
||||
}
|
||||
|
||||
// broadcast to any duplicate connections in channel
|
||||
server.broadcast( mutedPayload, { channel: socket.channel, hash: socket.hash });
|
||||
|
||||
// broadcast to allies, if any
|
||||
if(core.muzzledHashes[socket.hash].allies){
|
||||
server.broadcast( mutedPayload, { channel: socket.channel, nick: core.muzzledHashes[socket.hash].allies });
|
||||
}
|
||||
|
||||
// blanket "spam" protection, may expose the ratelimiting lines from `chat` and use that, TODO: one day #lazydev
|
||||
server._police.frisk(socket.remoteAddress, 9);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// shadow-prevent all invites from muzzled users
|
||||
exports.inviteCheck = (core, server, socket, payload) => {
|
||||
if (typeof payload.nick !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(core.muzzledHashes[socket.hash]){
|
||||
// generate common channel
|
||||
let channel = Math.random().toString(36).substr(2, 8);
|
||||
|
||||
// send fake reply
|
||||
server.reply({
|
||||
cmd: 'info',
|
||||
text: `You invited ${payload.nick} to ?${channel}`
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['nick'];
|
||||
|
||||
exports.info = {
|
||||
name: 'dumb',
|
||||
usage: 'dumb {nick} [allies...]',
|
||||
description: 'Globally shadow mute a connection. Optional allies array will see muted messages.'
|
||||
description: 'Globally shadow mute a connection. Optional allies array will see muted messages.',
|
||||
usage: `
|
||||
API: { cmd: 'dumb', nick: '<target nick>', allies: ['<optional nick array>', ...] }`
|
||||
};
|
||||
exports.info.aliases = ['muzzle', 'mute'];
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
Description: Forces a change on the target(s) socket's channel, then broadcasts event
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (socket.uType == 'user') {
|
||||
server._police.frisk(socket.remoteAddress, 10);
|
||||
|
||||
return;
|
||||
if (socket.uType === 'user') {
|
||||
return server._police.frisk(socket.remoteAddress, 10);
|
||||
}
|
||||
|
||||
// check user input
|
||||
|
@ -21,12 +20,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
let badClients = server.findSockets({ channel: socket.channel, nick: data.nick });
|
||||
|
||||
if (badClients.length === 0) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user(s) in channel'
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check if found targets are kickable, commit kick
|
||||
|
@ -75,10 +72,11 @@ exports.run = async (core, server, socket, data) => {
|
|||
core.managers.stats.increment('users-kicked', kicked.length);
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['nick'];
|
||||
|
||||
exports.info = {
|
||||
name: 'kick',
|
||||
usage: 'kick {nick}',
|
||||
description: 'Silently forces target client(s) into another channel. `nick` may be string or array of strings'
|
||||
description: 'Silently forces target client(s) into another channel. `nick` may be string or array of strings',
|
||||
usage: `
|
||||
API: { cmd: 'kick', nick: '<target nick>' }`
|
||||
};
|
||||
|
|
105
server/src/commands/mod/moveuser.js
Normal file
105
server/src/commands/mod/moveuser.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
Description: Removes the target socket from the current channel and forces a join event in another
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (socket.uType === 'user') {
|
||||
return server._police.frisk(socket.remoteAddress, 10);
|
||||
}
|
||||
|
||||
// check user input
|
||||
if (typeof data.nick !== 'string' || typeof data.channel !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.channel === socket.channel) {
|
||||
// moving them into the same channel? y u do this?
|
||||
return;
|
||||
}
|
||||
|
||||
let badClients = server.findSockets({ channel: socket.channel, nick: data.nick });
|
||||
|
||||
if (badClients.length === 0) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in channel'
|
||||
}, socket);
|
||||
}
|
||||
|
||||
let badClient = badClients[0];
|
||||
|
||||
if (badClient.uType !== 'user') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Cannot move other mods, how rude'
|
||||
}, socket);
|
||||
}
|
||||
|
||||
const currentNick = badClient.nick.toLowerCase();
|
||||
let userExists = server.findSockets({
|
||||
channel: data.channel,
|
||||
nick: (targetNick) => targetNick.toLowerCase() === currentNick
|
||||
});
|
||||
|
||||
if (userExists.length > 0) {
|
||||
// That nickname is already in that channel
|
||||
return;
|
||||
}
|
||||
|
||||
let peerList = server.findSockets({ channel: socket.channel });
|
||||
|
||||
if (peerList.length > 1) {
|
||||
for (let i = 0, l = peerList.length; i < l; i++) {
|
||||
server.reply({
|
||||
cmd: 'onlineRemove',
|
||||
nick: peerList[i].nick
|
||||
}, badClient);
|
||||
|
||||
if (badClient.nick !== peerList[i].nick){
|
||||
server.reply({
|
||||
cmd: 'onlineRemove',
|
||||
nick: badClient.nick
|
||||
}, peerList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let newPeerList = server.findSockets({ channel: data.channel });
|
||||
let moveAnnouncement = {
|
||||
cmd: 'onlineAdd',
|
||||
nick: badClient.nick,
|
||||
trip: badClient.trip || 'null',
|
||||
hash: server.getSocketHash(badClient)
|
||||
};
|
||||
let nicks = [];
|
||||
|
||||
for (let i = 0, l = newPeerList.length; i < l; i++) {
|
||||
server.reply(moveAnnouncement, newPeerList[i]);
|
||||
nicks.push(newPeerList[i].nick);
|
||||
}
|
||||
|
||||
nicks.push(badClient.nick);
|
||||
|
||||
server.reply({
|
||||
cmd: 'onlineSet',
|
||||
nicks: nicks
|
||||
}, badClient);
|
||||
|
||||
badClient.channel = data.channel;
|
||||
|
||||
server.broadcast( {
|
||||
cmd: 'info',
|
||||
text: `${badClient.nick} was moved into ?${data.channel}`
|
||||
}, { channel: data.channel });
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.requiredData = ['nick', 'channel'];
|
||||
exports.info = {
|
||||
name: 'moveuser',
|
||||
description: 'This will move the target user nick into another channel',
|
||||
usage: `
|
||||
API: { cmd: 'moveuser', nick: '<target nick>', channel: '<new channel>' }`
|
||||
};
|
|
@ -3,22 +3,26 @@
|
|||
* Author: simple
|
||||
*/
|
||||
|
||||
// module constructor
|
||||
exports.init = (core) => {
|
||||
if (typeof core.muzzledHashes === 'undefined') {
|
||||
core.muzzledHashes = {};
|
||||
}
|
||||
};
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (socket.uType == 'user') {
|
||||
server._police.frisk(socket.remoteAddress, 10);
|
||||
|
||||
return;
|
||||
if (socket.uType === 'user') {
|
||||
return server._police.frisk(socket.remoteAddress, 10);
|
||||
}
|
||||
|
||||
// check user input
|
||||
if (typeof data.ip !== 'string' && typeof data.hash !== 'string') {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: "hash:'targethash' or ip:'1.2.3.4' is required"
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// find target & remove mute status
|
||||
|
@ -36,10 +40,13 @@ exports.run = async (core, server, socket, data) => {
|
|||
cmd: 'info',
|
||||
text: `${socket.nick} unmuzzled : ${target}`
|
||||
}, { uType: 'mod' });
|
||||
}
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'speak',
|
||||
usage: 'speak {[ip || hash]}',
|
||||
description: 'Pardon a dumb user to be able to speak again'
|
||||
description: 'Pardon a dumb user to be able to speak again',
|
||||
usage: `
|
||||
API: { cmd: 'speak', ip/hash: '<target ip or hash' }`
|
||||
};
|
||||
exports.info.aliases = ['unmuzzle', 'unmute'];
|
||||
|
|
|
@ -2,22 +2,19 @@
|
|||
Description: Removes a target ip from the ratelimiter
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (socket.uType == 'user') {
|
||||
server._police.frisk(socket.remoteAddress, 10);
|
||||
|
||||
return;
|
||||
if (socket.uType === 'user') {
|
||||
return server._police.frisk(socket.remoteAddress, 10);
|
||||
}
|
||||
|
||||
// check user input
|
||||
if (typeof data.ip !== 'string' && typeof data.hash !== 'string') {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: "hash:'targethash' or ip:'1.2.3.4' is required"
|
||||
}, socket);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// find target
|
||||
|
@ -55,8 +52,10 @@ exports.run = async (core, server, socket, data) => {
|
|||
core.managers.stats.decrement('users-banned');
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'unban',
|
||||
usage: 'unban {[ip || hash]}',
|
||||
description: 'Removes target ip from the ratelimiter'
|
||||
description: 'Removes target ip from the ratelimiter',
|
||||
usage: `
|
||||
API: { cmd: 'unban', ip/hash: '<target ip or hash>' }`
|
||||
};
|
||||
|
|
36
server/src/commands/mod/unbanall.js
Normal file
36
server/src/commands/mod/unbanall.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Description: Clears all bans and ratelimits
|
||||
*/
|
||||
|
||||
// module main
|
||||
exports.run = async (core, server, socket, data) => {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (socket.uType === 'user') {
|
||||
return server._police.frisk(socket.remoteAddress, 10);
|
||||
}
|
||||
|
||||
// remove arrest records
|
||||
server._police._records = {};
|
||||
|
||||
console.log(`${socket.nick} [${socket.trip}] unbanned all`);
|
||||
|
||||
// reply with success
|
||||
server.reply({
|
||||
cmd: 'info',
|
||||
text: `Unbanned all ip addresses`
|
||||
}, socket);
|
||||
|
||||
// notify mods
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `${socket.nick} unbanned all ip addresses`
|
||||
}, { uType: 'mod' });
|
||||
};
|
||||
|
||||
// module meta
|
||||
exports.info = {
|
||||
name: 'unbanall',
|
||||
description: 'Clears all banned ip addresses',
|
||||
usage: `
|
||||
API: { cmd: 'unbanall' }`
|
||||
};
|
|
@ -10,8 +10,8 @@
|
|||
const wsServer = require('ws').Server;
|
||||
const socketReady = require('ws').OPEN;
|
||||
const crypto = require('crypto');
|
||||
const ipSalt = (Math.random().toString(36).substring(2, 16) + Math.random().toString(36).substring(2, (Math.random() * 16))).repeat(16);
|
||||
const internalCmdKey = (Math.random().toString(36).substring(2, 16) + Math.random().toString(36).substring(2, (Math.random() * 16))).repeat(16);
|
||||
const ipSalt = [...Array(Math.floor(Math.random()*128)+128)].map(i=>(~~(Math.random()*36)).toString(36)).join('');
|
||||
const internalCmdKey = [...Array(Math.floor(Math.random()*128)+128)].map(i=>(~~(Math.random()*36)).toString(36)).join('');
|
||||
const Police = require('./rateLimiter');
|
||||
const pulseSpeed = 16000; // ping all clients every X ms
|
||||
|
||||
|
@ -25,6 +25,7 @@ class server extends wsServer {
|
|||
super({ port: core.config.websocketPort });
|
||||
|
||||
this._core = core;
|
||||
this._hooks = {};
|
||||
this._police = new Police();
|
||||
this._cmdBlacklist = {};
|
||||
this._cmdKey = internalCmdKey;
|
||||
|
@ -40,6 +41,8 @@ class server extends wsServer {
|
|||
this.on('connection', (socket, request) => {
|
||||
this.newConnection(socket, request);
|
||||
});
|
||||
|
||||
this._core.commands.initCommandHooks(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,42 +108,59 @@ class server extends wsServer {
|
|||
// Penalize here, but don't do anything about it
|
||||
this._police.frisk(socket.remoteAddress, 1);
|
||||
|
||||
// ignore ridiculously large packets
|
||||
// Ignore ridiculously large packets
|
||||
if (data.length > 65536) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start sent data verification
|
||||
var args = null;
|
||||
var payload = null;
|
||||
try {
|
||||
args = JSON.parse(data);
|
||||
payload = JSON.parse(data);
|
||||
} catch (e) {
|
||||
// Client sent malformed json, gtfo
|
||||
socket.close();
|
||||
}
|
||||
|
||||
if (args === null) {
|
||||
if (payload === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof args.cmd === 'undefined') {
|
||||
if (typeof payload.cmd === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof args.cmd !== 'string') {
|
||||
if (typeof payload.cmd !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof socket.channel === 'undefined' && args.cmd !== 'join') {
|
||||
if (typeof socket.channel === 'undefined' && (payload.cmd !== 'join' && payload.cmd !== 'chat')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this._cmdBlacklist[args.cmd] === 'function') {
|
||||
if (typeof this._cmdBlacklist[payload.cmd] === 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Finished verification, pass to command modules
|
||||
this._core.commands.handleCommand(this, socket, args);
|
||||
// Execute `in` (incoming data) hooks and process results
|
||||
payload = this.executeHooks('in', socket, payload);
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
// A hook malfunctioned, reply with error
|
||||
this._core.commands.handleCommand(this, socket, {
|
||||
cmd: 'socketreply',
|
||||
cmdKey: this._cmdKey,
|
||||
text: payload
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (payload === false) {
|
||||
// A hook requested this data be dropped
|
||||
return;
|
||||
}
|
||||
|
||||
// Finished verification & hooks, pass to command modules
|
||||
this._core.commands.handleCommand(this, socket, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -168,16 +188,33 @@ class server extends wsServer {
|
|||
/**
|
||||
* Send data payload to specific socket/client
|
||||
*
|
||||
* @param {Object} data Object to convert to json for transmission
|
||||
* @param {Object} payload Object to convert to json for transmission
|
||||
* @param {Object} socket The target client
|
||||
*/
|
||||
send (data, socket) {
|
||||
send (payload, socket) {
|
||||
// Add timestamp to command
|
||||
data.time = Date.now();
|
||||
payload.time = Date.now();
|
||||
|
||||
// Execute `in` (incoming data) hooks and process results
|
||||
payload = this.executeHooks('out', socket, payload);
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
// A hook malfunctioned, reply with error
|
||||
this._core.commands.handleCommand(this, socket, {
|
||||
cmd: 'socketreply',
|
||||
cmdKey: this._cmdKey,
|
||||
text: payload
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (payload === false) {
|
||||
// A hook requested this data be dropped
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (socket.readyState === socketReady) {
|
||||
socket.send(JSON.stringify(data));
|
||||
socket.send(JSON.stringify(payload));
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
@ -185,20 +222,20 @@ class server extends wsServer {
|
|||
/**
|
||||
* Overload function for `this.send()`
|
||||
*
|
||||
* @param {Object} data Object to convert to json for transmission
|
||||
* @param {Object} payload Object to convert to json for transmission
|
||||
* @param {Object} socket The target client
|
||||
*/
|
||||
reply (data, socket) {
|
||||
this.send(data, socket);
|
||||
reply (payload, socket) {
|
||||
this.send(payload, 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} payload Object to convert to json for transmission
|
||||
* @param {Object} filter see `this.findSockets()`
|
||||
*/
|
||||
broadcast (data, filter) {
|
||||
broadcast (payload, filter) {
|
||||
let targetSockets = this.findSockets(filter);
|
||||
|
||||
if (targetSockets.length === 0) {
|
||||
|
@ -206,7 +243,7 @@ class server extends wsServer {
|
|||
}
|
||||
|
||||
for (let i = 0, l = targetSockets.length; i < l; i++) {
|
||||
this.send(data, targetSockets[i]);
|
||||
this.send(payload, targetSockets[i]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -271,7 +308,7 @@ class server extends wsServer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Encrypts target socket's remote address using non-static variable length salt
|
||||
* Hashes target socket's remote address using non-static variable length salt
|
||||
* encodes and shortens the output, returns that value
|
||||
*
|
||||
* @param {Object||String} target Either the target socket or ip as string
|
||||
|
@ -287,6 +324,73 @@ class server extends wsServer {
|
|||
|
||||
return sha.digest('base64').substr(0, 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a target function to an array of hooks. Hooks are executed either before
|
||||
* processing user input (`in`) or before sending data back to the client (`out`)
|
||||
* and allows a module to modify each payload before moving forward
|
||||
*
|
||||
* @param {String} type The type of event, typically `in` (incoming) or `out` (outgoing)
|
||||
* @param {String} command Should match the desired `cmd` attrib of the payload
|
||||
* @param {Function} hookFunction Target function to execute, should accept `server`, `socket` and `payload` as parameters
|
||||
*/
|
||||
// TODO: add hook priority levels
|
||||
registerHook (type, command, hookFunction) {
|
||||
if (typeof this._hooks[type] === 'undefined') {
|
||||
this._hooks[type] = new Map();
|
||||
}
|
||||
|
||||
if (!this._hooks[type].has(command)) {
|
||||
this._hooks[type].set(command, []);
|
||||
}
|
||||
|
||||
this._hooks[type].get(command).push(hookFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through registered hooks & processes the results. Returned data will
|
||||
* be one of three possiblities:
|
||||
* A payload (modified or not) that will continue through the data flow
|
||||
* A boolean false to indicate halting the data through flow
|
||||
* A string which indicates an error occured in executing the hook
|
||||
*
|
||||
* @param {String} type The type of event, typically `in` (incoming) or `out` (outgoing)
|
||||
* @param {Object} socket Either the target client or the client triggering the hook (depending on `type`)
|
||||
* @param {Object} payload Either incoming data from client or outgoing data (depending on `type`)
|
||||
*/
|
||||
executeHooks (type, socket, payload) {
|
||||
let command = payload.cmd;
|
||||
|
||||
if (typeof this._hooks[type] !== 'undefined') {
|
||||
if (this._hooks[type].has(command)) {
|
||||
let hooks = this._hooks[type].get(command);
|
||||
|
||||
for (let i = 0, j = hooks.length; i < j; i++) {
|
||||
try {
|
||||
payload = hooks[i](this._core, this, socket, payload);
|
||||
} catch (err) {
|
||||
let errText = `Hook failure, '${type}', '${command}': ${err}`;
|
||||
console.log(errText);
|
||||
return errText;
|
||||
}
|
||||
|
||||
// A payload may choose to return false to prevent all further processing
|
||||
if (payload === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipe server hooks to make ready for module reload calls
|
||||
*/
|
||||
clearHooks () {
|
||||
this._hooks = {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = server;
|
||||
|
|
|
@ -71,8 +71,9 @@ class CommandManager {
|
|||
|
||||
command.info.category = category;
|
||||
|
||||
if (this._categories.indexOf(category) === -1)
|
||||
if (this._categories.indexOf(category) === -1) {
|
||||
this._categories.push(category);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof command.init === 'function') {
|
||||
|
@ -114,7 +115,7 @@ class CommandManager {
|
|||
/**
|
||||
* Pulls all command names from a passed `category`
|
||||
*
|
||||
* @param {String} category reference to the newly loaded object
|
||||
* @param {String} category [Optional] filter return results by this category
|
||||
*/
|
||||
all (category) {
|
||||
return !category ? this._commands : this._commands.filter(c => c.info.category.toLowerCase() === category.toLowerCase());
|
||||
|
@ -148,6 +149,15 @@ class CommandManager {
|
|||
return this._commands.find(c => c.info[key] === value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `initHooks` function on any modules that utilize the event
|
||||
*
|
||||
* @param {Object} server main server object
|
||||
*/
|
||||
initCommandHooks (server) {
|
||||
this._commands.filter(c => typeof c.initHooks !== 'undefined').forEach(c => c.initHooks(server));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and executes the requested command, or fails with semi-intelligent error
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue
Block a user