four new modules
This commit is contained in:
@@ -48,7 +48,7 @@ export async function run({
|
||||
|
||||
for (let i = 0, l = targetMod.length; i < l; i += 1) {
|
||||
// downgrade privileges
|
||||
targetMod[i].uType = 'user'; /* @legacy */
|
||||
targetMod[i].uType = 'user';
|
||||
targetMod[i].level = levels.default;
|
||||
|
||||
// inform ex-mod
|
||||
|
||||
+33
-24
@@ -6,43 +6,51 @@
|
||||
* @module chat
|
||||
*/
|
||||
|
||||
import { parseText } from '../utility/_Text.js';
|
||||
import {
|
||||
parseText,
|
||||
} from '../utility/_Text.js';
|
||||
import {
|
||||
isAdmin,
|
||||
isModerator,
|
||||
} from '../utility/_UAC.js';
|
||||
|
||||
/**
|
||||
* Maximum length of the customId property
|
||||
* @type {number}
|
||||
*/
|
||||
export const MAX_MESSAGE_ID_LENGTH = 6;
|
||||
|
||||
/**
|
||||
* The time in milliseconds before a message is considered stale, and thus no longer allowed
|
||||
* to be edited.
|
||||
* @type {number}
|
||||
*/
|
||||
* The time in milliseconds before a message is considered stale, and thus no longer allowed
|
||||
* to be edited.
|
||||
* @type {number}
|
||||
*/
|
||||
const ACTIVE_TIMEOUT = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* The time in milliseconds that a check for stale messages should be performed.
|
||||
* @type {number}
|
||||
*/
|
||||
* The time in milliseconds that a check for stale messages should be performed.
|
||||
* @type {number}
|
||||
*/
|
||||
const TIMEOUT_CHECK_INTERVAL = 30 * 1000;
|
||||
|
||||
/**
|
||||
* Stores active messages that can be edited.
|
||||
* @type {{ customId: string, userid: number, sent: number }[]}
|
||||
*/
|
||||
* Stores active messages that can be edited.
|
||||
* @type {{ customId: string, userid: number, sent: number }[]}
|
||||
*/
|
||||
export const ACTIVE_MESSAGES = [];
|
||||
|
||||
/**
|
||||
* Cleans up stale messages.
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
* Cleans up stale messages.
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export function cleanActiveMessages() {
|
||||
const now = Date.now();
|
||||
for (let i = 0; i < ACTIVE_MESSAGES.length; i++) {
|
||||
for (let i = 0; i < ACTIVE_MESSAGES.length; i += 1) {
|
||||
const message = ACTIVE_MESSAGES[i];
|
||||
if (now - message.sent > ACTIVE_TIMEOUT || message.toDelete) {
|
||||
ACTIVE_MESSAGES.splice(i, 1);
|
||||
i--;
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,12 +59,12 @@ export function cleanActiveMessages() {
|
||||
setInterval(cleanActiveMessages, TIMEOUT_CHECK_INTERVAL);
|
||||
|
||||
/**
|
||||
* Adds a message to the active messages map.
|
||||
* @public
|
||||
* @param {string} id
|
||||
* @param {number} userid
|
||||
* @return {void}
|
||||
*/
|
||||
* Adds a message to the active messages map.
|
||||
* @public
|
||||
* @param {string} id
|
||||
* @param {number} userid
|
||||
* @return {void}
|
||||
*/
|
||||
export function addActiveMessage(customId, userid) {
|
||||
ACTIVE_MESSAGES.push({
|
||||
customId,
|
||||
@@ -93,7 +101,7 @@ export async function run({
|
||||
}, socket);
|
||||
}
|
||||
|
||||
const customId = payload.customId;
|
||||
const { customId } = payload;
|
||||
|
||||
if (typeof (customId) === 'string' && customId.length > MAX_MESSAGE_ID_LENGTH) {
|
||||
// There's a limit on the custom id length.
|
||||
@@ -127,6 +135,7 @@ export async function run({
|
||||
}
|
||||
|
||||
addActiveMessage(outgoingPayload.customId, socket.userid);
|
||||
|
||||
// broadcast to channel peers
|
||||
server.broadcast(outgoingPayload, { channel: socket.channel });
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function run({
|
||||
}
|
||||
|
||||
// `join` is the legacy entry point, check if it needs to be upgraded
|
||||
if (typeof socket.hcProtocol === 'undefined') {
|
||||
if (typeof socket.hcProtocol === 'undefined' || socket.hcProtocol === 1) {
|
||||
payload = upgradeLegacyJoin(server, socket, payload);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function run({ core, server, socket }) {
|
||||
// gather connection and channel count
|
||||
const ips = {};
|
||||
const channels = {};
|
||||
// @todo use public channels from core.appConfig.data
|
||||
// @todo use public channel flag
|
||||
const publicChanCounts = {
|
||||
lounge: 0,
|
||||
meta: 0,
|
||||
|
||||
+17
-17
@@ -2,9 +2,9 @@
|
||||
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Restore session
|
||||
* @summary Create or restore session
|
||||
* @version 1.0.0
|
||||
* @description Restore previous state by session
|
||||
* @description Restore previous state by session or create new session
|
||||
* @module session
|
||||
*/
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
const SessionLocation = './session.key';
|
||||
|
||||
/**
|
||||
* Get a fresh session string for target socket
|
||||
* @param {Object} socket
|
||||
* @param {Object} core
|
||||
* Get a new json web token for the provided socket
|
||||
* @param {*} socket
|
||||
* @param {*} core
|
||||
* @returns {object}
|
||||
*/
|
||||
export function getSession(socket, core) {
|
||||
@@ -39,7 +39,7 @@ export function getSession(socket, core) {
|
||||
nick: socket.nick,
|
||||
trip: socket.trip,
|
||||
userid: socket.userid,
|
||||
uType: socket.uType, /* @legacy */
|
||||
uType: socket.uType,
|
||||
muzzled: socket.muzzled || false,
|
||||
banned: socket.banned || false,
|
||||
}, core.sessionKey, {
|
||||
@@ -49,8 +49,8 @@ export function getSession(socket, core) {
|
||||
|
||||
/**
|
||||
* Reply to target socket with session failure notice
|
||||
* @param {Object} server
|
||||
* @param {Object} socket
|
||||
* @param {*} server
|
||||
* @param {*} socket
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function notifyFailure(server, socket) {
|
||||
@@ -136,13 +136,21 @@ export async function run({
|
||||
socket.nick = session.nick;
|
||||
socket.trip = session.trip;
|
||||
socket.userid = session.userid;
|
||||
socket.uType = session.uType; /* @legacy */
|
||||
socket.uType = session.uType;
|
||||
socket.muzzled = session.muzzled;
|
||||
socket.banned = session.banned;
|
||||
|
||||
socket.hash = server.getSocketHash(socket);
|
||||
socket.hcProtocol = 2;
|
||||
|
||||
// dispatch info
|
||||
server.reply({
|
||||
cmd: 'session',
|
||||
restored: true,
|
||||
token: getSession(socket, core),
|
||||
channels: socket.channels,
|
||||
}, socket);
|
||||
|
||||
for (let i = 0, j = session.channels.length; i < j; i += 1) {
|
||||
restoreJoin({
|
||||
core,
|
||||
@@ -152,14 +160,6 @@ export async function run({
|
||||
}, true);
|
||||
}
|
||||
|
||||
// dispatch updated session
|
||||
server.reply({
|
||||
cmd: 'session',
|
||||
restored: true,
|
||||
token: getSession(socket, core),
|
||||
channels: socket.channels,
|
||||
}, socket);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
import { parseText } from "../utility/_Text.js";
|
||||
import { isAdmin, isModerator } from "../utility/_UAC.js";
|
||||
import { ACTIVE_MESSAGES, MAX_MESSAGE_ID_LENGTH } from "./chat.js";
|
||||
/**
|
||||
* @author MinusGix ( https://github.com/MinusGix )
|
||||
* @summary Change target message
|
||||
* @version v1.0.0
|
||||
* @description Will alter a previously sent message using that message's customId
|
||||
* @module updateMessage
|
||||
*/
|
||||
|
||||
export async function run({ core, server, socket, payload }) {
|
||||
import {
|
||||
parseText,
|
||||
} from '../utility/_Text.js';
|
||||
import {
|
||||
isAdmin,
|
||||
isModerator,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
ACTIVE_MESSAGES,
|
||||
MAX_MESSAGE_ID_LENGTH,
|
||||
} from './chat.js';
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
server, socket, payload,
|
||||
}) {
|
||||
// undefined | "overwrite" | "append" | "prepend" | "complete"
|
||||
let mode = payload.mode;
|
||||
const { customId } = payload;
|
||||
let { mode, text } = payload;
|
||||
|
||||
if (!mode) {
|
||||
mode = 'overwrite';
|
||||
@@ -14,14 +39,10 @@ export async function run({ core, server, socket, payload }) {
|
||||
return server.police.frisk(socket.address, 13);
|
||||
}
|
||||
|
||||
const customId = payload.customId;
|
||||
|
||||
if (!customId || typeof customId !== "string" || customId.length > MAX_MESSAGE_ID_LENGTH) {
|
||||
if (!customId || typeof customId !== 'string' || customId.length > MAX_MESSAGE_ID_LENGTH) {
|
||||
return server.police.frisk(socket.address, 13);
|
||||
}
|
||||
|
||||
let text = payload.text;
|
||||
|
||||
if (typeof (text) !== 'string') {
|
||||
return server.police.frisk(socket.address, 13);
|
||||
}
|
||||
@@ -43,7 +64,7 @@ export async function run({ core, server, socket, payload }) {
|
||||
// Or flashing between huge and small. Etc.
|
||||
|
||||
let message;
|
||||
for (let i = 0; i < ACTIVE_MESSAGES.length; i++) {
|
||||
for (let i = 0; i < ACTIVE_MESSAGES.length; i += 1) {
|
||||
const msg = ACTIVE_MESSAGES[i];
|
||||
|
||||
if (msg.userid === socket.userid && msg.customId === customId) {
|
||||
@@ -80,6 +101,12 @@ export async function run({ core, server, socket, payload }) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following payload properties are required to invoke this module:
|
||||
* "text", "customId"
|
||||
* @public
|
||||
* @typedef {Array} addmod/requiredData
|
||||
*/
|
||||
export const requiredData = ['text', 'customId'];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Disables the captcha
|
||||
* @version 1.0.0
|
||||
* @description Disables the captcha on the channel specified in the channel property,
|
||||
* default is current channel
|
||||
* @module disablecaptcha
|
||||
*/
|
||||
|
||||
import {
|
||||
isModerator,
|
||||
} from '../utility/_UAC.js';
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready
|
||||
* @param {Object} core - Reference to core enviroment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function init(core) {
|
||||
if (typeof core.captchas === 'undefined') {
|
||||
core.captchas = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (!isModerator(socket.level)) {
|
||||
return server.police.frisk(socket.address, 10);
|
||||
}
|
||||
|
||||
let targetChannel;
|
||||
|
||||
if (typeof payload.channel !== 'string') {
|
||||
if (typeof socket.channel !== 'string') { // @todo Multichannel
|
||||
return false; // silently fail
|
||||
}
|
||||
|
||||
targetChannel = socket.channel;
|
||||
} else {
|
||||
targetChannel = payload.channel;
|
||||
}
|
||||
|
||||
if (!core.captchas[targetChannel]) {
|
||||
return server.reply({
|
||||
cmd: 'info',
|
||||
text: 'Captcha is not enabled.',
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
core.captchas[targetChannel] = false;
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `Captcha disabled on: ${targetChannel}`,
|
||||
channel: false, // @todo Multichannel, false for global info
|
||||
}, { channel: targetChannel, level: isModerator });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} disablecaptcha/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'disablecaptcha',
|
||||
category: 'moderators',
|
||||
description: 'Disables the captcha on the channel specified in the channel property, default is current channel',
|
||||
usage: `
|
||||
API: { cmd: 'disablecaptcha', channel: '<optional channel, defaults to your current channel' }`,
|
||||
};
|
||||
@@ -361,6 +361,7 @@ export function whisperCheck({
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {Array} aliases - An array of alternative cmd names
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/* eslint no-param-reassign: 0 */
|
||||
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Enables the captcha
|
||||
* @version 1.0.0
|
||||
* @description Enables the captcha on the channel specified in the channel property,
|
||||
* default is current channel
|
||||
* @module enablecaptcha
|
||||
*/
|
||||
|
||||
import captcha from 'ascii-captcha';
|
||||
|
||||
import {
|
||||
isTrustedUser,
|
||||
isModerator,
|
||||
verifyNickname,
|
||||
getUserPerms,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
canJoinChannel,
|
||||
} from '../utility/_Channels.js';
|
||||
import {
|
||||
upgradeLegacyJoin,
|
||||
legacyLevelToLabel,
|
||||
} from '../utility/_LegacyFunctions.js';
|
||||
import {
|
||||
Errors,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready
|
||||
* @param {Object} core - Reference to core enviroment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function init(core) {
|
||||
if (typeof core.captchas === 'undefined') {
|
||||
core.captchas = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (!isModerator(socket.level)) {
|
||||
return server.police.frisk(socket.address, 10);
|
||||
}
|
||||
|
||||
let targetChannel;
|
||||
|
||||
if (typeof payload.channel !== 'string') {
|
||||
if (typeof socket.channel !== 'string') { // @todo Multichannel
|
||||
return false; // silently fail
|
||||
}
|
||||
|
||||
targetChannel = socket.channel;
|
||||
} else {
|
||||
targetChannel = payload.channel;
|
||||
}
|
||||
|
||||
if (core.captchas[targetChannel]) {
|
||||
return server.reply({
|
||||
cmd: 'info',
|
||||
text: 'Captcha is already enabled.',
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
core.captchas[targetChannel] = true;
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `Captcha enabled on: ${targetChannel}`,
|
||||
channel: socket.channel, // @todo Multichannel, false for global info
|
||||
}, { channel: socket.channel, level: isModerator });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready to register this modules hooks
|
||||
* @param {Object} server - Reference to server enviroment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export function initHooks(server) {
|
||||
server.registerHook('in', 'chat', this.chatCheck.bind(this), 5);
|
||||
server.registerHook('in', 'join', this.joinCheck.bind(this), 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming chat command is invoked;
|
||||
* hook incoming chat commands, check if they are answering a captcha
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {{Object|boolean|string}} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function chatCheck({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// always verifiy user input
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof socket.captcha !== 'undefined') {
|
||||
if (socket.captcha.awaiting === true) {
|
||||
if (payload.text === socket.captcha.solution) {
|
||||
if (typeof socket.captcha.whitelist === 'undefined') {
|
||||
socket.captcha.whitelist = [];
|
||||
}
|
||||
|
||||
socket.captcha.whitelist.push(socket.captcha.origChannel);
|
||||
socket.captcha.awaiting = false;
|
||||
|
||||
if (socket.hcProtocol === 1) {
|
||||
core.commands.handleCommand(server, socket, {
|
||||
cmd: 'join',
|
||||
nick: `${socket.captcha.origNick}#${socket.captcha.origPass}`,
|
||||
channel: socket.captcha.origChannel,
|
||||
});
|
||||
} else {
|
||||
core.commands.handleCommand(server, socket, {
|
||||
cmd: 'join',
|
||||
nick: socket.captcha.origNick,
|
||||
pass: socket.captcha.origPass,
|
||||
channel: socket.captcha.origChannel,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
server.police.frisk(socket.address, 7);
|
||||
socket.terminate();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming join command is invoked;
|
||||
* hook incoming join commands, check if they are joining a captcha protected channel
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {{Object|boolean|string}} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function joinCheck({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// check if channel has captcha enabled
|
||||
if (core.captchas[payload.channel] !== true) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
// `join` is the legacy entry point, check if it needs to be upgraded
|
||||
const origPayload = { ...payload };
|
||||
if (typeof socket.hcProtocol === 'undefined') {
|
||||
payload = upgradeLegacyJoin(server, socket, payload);
|
||||
}
|
||||
|
||||
// store payload values
|
||||
const { channel, nick, pass } = payload;
|
||||
|
||||
// check if a client is able to join target channel
|
||||
const mayJoin = canJoinChannel(channel, socket);
|
||||
if (mayJoin !== true) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You may not join that channel.',
|
||||
id: mayJoin,
|
||||
channel: false, // @todo Multichannel, false for global event
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// calling socket already in a channel
|
||||
// @todo multichannel update, will remove
|
||||
if (typeof socket.channel !== 'undefined') {
|
||||
return server.reply({
|
||||
cmd: 'warn', // @todo Remove this
|
||||
text: 'Joining more than one channel is not currently supported',
|
||||
id: Errors.Join.ALREADY_JOINED,
|
||||
channel: false, // @todo Multichannel, false for global event
|
||||
}, socket);
|
||||
}
|
||||
// end todo
|
||||
|
||||
// validates the user input for `nick`
|
||||
if (verifyNickname(nick, socket) !== true) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
|
||||
id: Errors.Join.INVALID_NICK,
|
||||
channel: false, // @todo Multichannel, false for global event
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// get trip and level
|
||||
const { trip, level } = getUserPerms(pass, core.saltKey, core.appConfig.data, channel);
|
||||
|
||||
// store the user values
|
||||
const userInfo = {
|
||||
nick,
|
||||
trip,
|
||||
uType: legacyLevelToLabel(level),
|
||||
hash: socket.hash,
|
||||
level,
|
||||
userid: socket.userid,
|
||||
isBot: socket.isBot,
|
||||
color: socket.color,
|
||||
channel,
|
||||
};
|
||||
|
||||
if (userInfo.uType === 'user') {
|
||||
if (userInfo.trip == null || isTrustedUser(level) === false) {
|
||||
if (typeof socket.captcha === 'undefined') {
|
||||
socket.captcha = {
|
||||
awaiting: true,
|
||||
origChannel: payload.channel,
|
||||
origNick: payload.nick,
|
||||
origPass: pass,
|
||||
solution: captcha.generateRandomText(6),
|
||||
};
|
||||
|
||||
server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Enter the following to join (case-sensitive):',
|
||||
channel: payload.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
server.reply({
|
||||
cmd: 'captcha',
|
||||
text: captcha.word2Transformedstr(socket.captcha.solution),
|
||||
channel: payload.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
socket.terminate();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return origPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} enablecaptcha/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'enablecaptcha',
|
||||
category: 'moderators',
|
||||
description: 'Enables a captcha in the current channel you are in',
|
||||
usage: `
|
||||
API: { cmd: 'enablecaptcha', channel: '<optional channel, defaults to your current channel>' }`,
|
||||
};
|
||||
@@ -0,0 +1,332 @@
|
||||
/* eslint no-param-reassign: 0 */
|
||||
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Locks the channel
|
||||
* @version 1.0.0
|
||||
* @description Locks a channel preventing default levels from joining
|
||||
* @module lockroom
|
||||
*/
|
||||
|
||||
import {
|
||||
levels,
|
||||
isTrustedUser,
|
||||
isModerator,
|
||||
verifyNickname,
|
||||
getUserPerms,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
upgradeLegacyJoin,
|
||||
legacyLevelToLabel,
|
||||
} from '../utility/_LegacyFunctions.js';
|
||||
import {
|
||||
Errors,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
canJoinChannel,
|
||||
} from '../utility/_Channels.js';
|
||||
|
||||
const danteQuotes = [
|
||||
'Do not be afraid; our fate cannot be taken from us; it is a gift.',
|
||||
'In the middle of the journey of our life I found myself within a dark woods where the straight way was lost.',
|
||||
'There is no greater sorrow then to recall our times of joy in wretchedness.',
|
||||
'They yearn for what they fear for.',
|
||||
'Through me you go into a city of weeping; through me you go into eternal pain; through me you go amongst the lost people',
|
||||
'From there we came outside and saw the stars',
|
||||
'But the stars that marked our starting fall away. We must go deeper into greater pain, for it is not permitted that we stay.',
|
||||
'Hope not ever to see Heaven. I have come to lead you to the other shore; into eternal darkness; into fire and into ice.',
|
||||
'As little flowers, which the chill of night has bent and huddled, when the white sun strikes, grow straight and open fully on their stems, so did I, too, with my exhausted force.',
|
||||
'At grief so deep the tongue must wag in vain; the language of our sense and memory lacks the vocabulary of such pain.',
|
||||
'Thence we came forth to rebehold the stars.',
|
||||
'He is, most of all, l\'amor che move il sole e l\'altre stelle.',
|
||||
'The poets leave hell and again behold the stars.',
|
||||
'One ought to be afraid of nothing other then things possessed of power to do us harm, but things innoucuous need not be feared.',
|
||||
'As phantoms frighten beasts when shadows fall.',
|
||||
'We were men once, though we\'ve become trees',
|
||||
'Here pity only lives when it is dead',
|
||||
'Lasciate ogne speranza, voi ch\'intrate.',
|
||||
'There is no greater sorrow than thinking back upon a happy time in misery',
|
||||
'My thoughts were full of other things When I wandered off the path.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready
|
||||
* @param {Object} core - Reference to core enviroment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function init(core) {
|
||||
if (typeof core.locked === 'undefined') {
|
||||
core.locked = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (!isModerator(socket.level)) {
|
||||
return server.police.frisk(socket.address, 10);
|
||||
}
|
||||
|
||||
let targetChannel;
|
||||
|
||||
if (typeof payload.channel !== 'string') {
|
||||
if (typeof socket.channel !== 'string') { // @todo Multichannel
|
||||
return false; // silently fail
|
||||
}
|
||||
|
||||
targetChannel = socket.channel;
|
||||
} else {
|
||||
targetChannel = payload.channel;
|
||||
}
|
||||
|
||||
if (core.locked[targetChannel]) {
|
||||
return server.reply({
|
||||
cmd: 'info',
|
||||
text: 'Channel is already locked.',
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// apply lock flag to channel list
|
||||
core.locked[targetChannel] = true;
|
||||
|
||||
// inform mods
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `Channel: ?${targetChannel} lock enabled by [${socket.trip}]${socket.nick}`,
|
||||
channel: false, // @todo Multichannel, false for global info
|
||||
}, { level: isModerator });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready to register this modules hooks
|
||||
* @param {Object} server - Reference to server enviroment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export function initHooks(server) {
|
||||
server.registerHook('in', 'changenick', this.changeNickCheck.bind(this), 1);
|
||||
server.registerHook('in', 'whisper', this.whisperCheck.bind(this), 1);
|
||||
server.registerHook('in', 'chat', this.chatCheck.bind(this), 1);
|
||||
server.registerHook('in', 'invite', this.inviteCheck.bind(this), 1);
|
||||
server.registerHook('in', 'join', this.joinCheck.bind(this), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming changenick command is invoked;
|
||||
* hook incoming changenick commands, reject them if the channel is 'purgatory'
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {{Object|boolean|string}} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function changeNickCheck({
|
||||
socket, payload,
|
||||
}) {
|
||||
if (socket.channel === 'purgatory') { // @todo Multichannel update
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming whisper command is invoked;
|
||||
* hook incoming whisper commands, reject them if the channel is 'purgatory'
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {{Object|boolean|string}} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function whisperCheck({
|
||||
socket, payload,
|
||||
}) {
|
||||
if (socket.channel === 'purgatory') { // @todo Multichannel update
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming chat command is invoked;
|
||||
* hook incoming chat commands, reject them if the channel is 'purgatory'
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {{Object|boolean|string}} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function chatCheck({
|
||||
socket, payload,
|
||||
}) {
|
||||
if (socket.channel === 'purgatory') {
|
||||
if (socket.level >= levels.moderator) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming invite command is invoked;
|
||||
* hook incoming invite commands, reject them if the channel is 'purgatory'
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {{Object|boolean|string}} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function inviteCheck({
|
||||
socket, payload,
|
||||
}) {
|
||||
if (socket.channel === 'purgatory') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming join command is invoked;
|
||||
* hook incoming join commands, shunt them to purgatory if needed
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {{Object|boolean|string}} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function joinCheck({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// check if target channel is locked
|
||||
if (typeof core.locked[payload.channel] === 'undefined' || core.locked[payload.channel] !== true) {
|
||||
if (payload.channel !== 'purgatory') {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
// `join` is the legacy entry point, check if it needs to be upgraded
|
||||
if (typeof socket.hcProtocol === 'undefined') {
|
||||
payload = upgradeLegacyJoin(server, socket, payload);
|
||||
}
|
||||
|
||||
// store payload values
|
||||
const { channel, nick, pass } = payload;
|
||||
|
||||
// check if a client is able to join target channel
|
||||
const mayJoin = canJoinChannel(channel, socket);
|
||||
if (mayJoin !== true) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You may not join that channel.',
|
||||
id: mayJoin,
|
||||
channel: false, // @todo Multichannel, false for global event
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// calling socket already in a channel
|
||||
// @todo multichannel update, will remove
|
||||
if (typeof socket.channel !== 'undefined') {
|
||||
return server.reply({
|
||||
cmd: 'warn', // @todo Remove this
|
||||
text: 'Joining more than one channel is not currently supported',
|
||||
id: Errors.Join.ALREADY_JOINED,
|
||||
channel: false, // @todo Multichannel, false for global event
|
||||
}, socket);
|
||||
}
|
||||
// end todo
|
||||
|
||||
// validates the user input for `nick`
|
||||
if (verifyNickname(nick, socket) !== true) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
|
||||
id: Errors.Join.INVALID_NICK,
|
||||
channel: false, // @todo Multichannel, false for global event
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// get trip and level
|
||||
const { trip, level } = getUserPerms(pass, core.saltKey, core.appConfig.data, channel);
|
||||
|
||||
// store the user values
|
||||
const userInfo = {
|
||||
nick,
|
||||
trip,
|
||||
uType: legacyLevelToLabel(level),
|
||||
hash: socket.hash,
|
||||
level,
|
||||
userid: socket.userid,
|
||||
isBot: socket.isBot,
|
||||
color: socket.color,
|
||||
channel,
|
||||
};
|
||||
|
||||
// check if trip is allowed
|
||||
if (userInfo.uType === 'user') {
|
||||
if (userInfo.trip == null || isTrustedUser(level) === false) {
|
||||
const origNick = userInfo.nick;
|
||||
const origChannel = payload.channel;
|
||||
|
||||
// not allowed, shunt to purgatory
|
||||
payload.channel = 'purgatory';
|
||||
|
||||
// lost souls have no names
|
||||
if (origChannel === 'purgatory') {
|
||||
// someone is pulling a Dante
|
||||
payload.nick = `Dante_${Math.random().toString(36).substr(2, 8)}`;
|
||||
} else {
|
||||
payload.nick = `${Math.random().toString(36).substr(2, 8)}${Math.random().toString(36).substr(2, 8)}`;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
server.reply({
|
||||
cmd: 'info',
|
||||
text: danteQuotes[Math.floor(Math.random() * danteQuotes.length)],
|
||||
channel: 'purgatory', // @todo Multichannel
|
||||
}, socket);
|
||||
}, 100);
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `${payload.nick} is: ${origNick}\ntrip: ${userInfo.trip || 'none'}\ntried to join: ?${origChannel}\nhash: ${userInfo.hash}`,
|
||||
channel: 'purgatory', // @todo Multichannel, false for global info
|
||||
}, { channel: 'purgatory', level: isModerator });
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} kick/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'lockroom',
|
||||
category: 'moderators',
|
||||
description: 'Locks a channel preventing default levels from joining',
|
||||
usage: `
|
||||
API: { cmd: 'lockroom', channel: '<optional channel, defaults to your current channel>' }`,
|
||||
};
|
||||
@@ -94,6 +94,7 @@ export async function run({
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {Array} aliases - An array of alternative cmd names
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
@@ -102,5 +103,5 @@ export const info = {
|
||||
description: 'Pardon a dumb user to be able to speak again',
|
||||
aliases: ['unmuzzle', 'unmute'],
|
||||
usage: `
|
||||
API: { cmd: 'speak', ip/hash: '<target ip or hash' }`,
|
||||
API: { cmd: 'speak', ip/hash: '<target ip or hash>' }`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Unlock target channel
|
||||
* @version 1.0.0
|
||||
* @description Unlocks a channel allowing anyone to join
|
||||
* @module unlockroom
|
||||
*/
|
||||
|
||||
import {
|
||||
isModerator,
|
||||
} from '../utility/_UAC.js';
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready
|
||||
* @param {Object} core - Reference to core enviroment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function init(core) {
|
||||
if (typeof core.locked === 'undefined') {
|
||||
core.locked = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (!isModerator(socket.level)) {
|
||||
return server.police.frisk(socket.address, 10);
|
||||
}
|
||||
|
||||
let targetChannel;
|
||||
|
||||
if (typeof payload.channel !== 'string') {
|
||||
if (typeof socket.channel !== 'string') { // @todo Multichannel
|
||||
return false; // silently fail
|
||||
}
|
||||
|
||||
targetChannel = socket.channel;
|
||||
} else {
|
||||
targetChannel = payload.channel;
|
||||
}
|
||||
|
||||
if (!core.locked[targetChannel]) {
|
||||
return server.reply({
|
||||
cmd: 'info',
|
||||
text: 'Channel is not locked.',
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
core.locked[targetChannel] = false;
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `Channel: ?${targetChannel} unlocked by [${socket.trip}]${socket.nick}`,
|
||||
channel: targetChannel, // @todo Multichannel, false for global info
|
||||
}, { channel: targetChannel, level: isModerator });
|
||||
|
||||
console.log(`Channel: ?${targetChannel} unlocked by [${socket.trip}]${socket.nick} in ${socket.channel}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} unlockroom/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'unlockroom',
|
||||
category: 'moderators',
|
||||
description: 'Unlock the current channel you are in or target channel as specified',
|
||||
usage: `
|
||||
API: { cmd: 'unlockroom', channel: '<optional target channel>' }`,
|
||||
};
|
||||
@@ -1,3 +1,13 @@
|
||||
/* eslint import/prefer-default-export: 0 */
|
||||
|
||||
/**
|
||||
* @author MinusGix ( https://github.com/MinusGix )
|
||||
* @summary General string helper functions
|
||||
* @version v1.0.0
|
||||
* @description A library of several commonly used string functions
|
||||
* @module Text
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check and trim string provided by remote client
|
||||
* @public
|
||||
|
||||
@@ -15,8 +15,6 @@ const {
|
||||
createHash,
|
||||
} = await import('crypto');
|
||||
|
||||
const TripLength = 10;
|
||||
|
||||
/**
|
||||
* Object defining labels for default permission ranges
|
||||
* @typedef {Object} levels
|
||||
@@ -110,7 +108,7 @@ export function getUserDetails(socket) {
|
||||
return {
|
||||
nick: socket.nick,
|
||||
trip: socket.trip || '',
|
||||
uType: socket.uType, /* @legacy */
|
||||
uType: socket.uType,
|
||||
hash: socket.hash,
|
||||
level: socket.level,
|
||||
userid: socket.userid,
|
||||
@@ -152,7 +150,7 @@ export function getUserPerms(pass, salt, config, channel) {
|
||||
|
||||
const sha = createHash('sha256');
|
||||
sha.update(pass + salt);
|
||||
const trip = sha.digest('base64').substr(0, TripLength);
|
||||
const trip = sha.digest('base64').substr(0, 6);
|
||||
|
||||
// check if user is global admin
|
||||
if (trip === config.adminTrip) {
|
||||
|
||||
Reference in New Issue
Block a user