diff --git a/client/client.js b/client/client.js index 682a28e..5b902bc 100644 --- a/client/client.js +++ b/client/client.js @@ -353,7 +353,9 @@ function join(channel) { var args = JSON.parse(message.data); var cmd = args.cmd; var command = COMMANDS[cmd]; - command.call(null, args); + if (command) { + command.call(null, args); + } } } @@ -368,9 +370,9 @@ var COMMANDS = { info: function (args) { args.nick = '*'; pushMessage(args); - }, + }, - emote: function (args) { + emote: function (args) { args.nick = '*'; pushMessage(args); }, @@ -410,6 +412,30 @@ var COMMANDS = { if ($('#joined-left').checked) { pushMessage({ nick: '*', text: nick + " left" }); } + }, + + captcha: function (args) { + var messageEl = document.createElement('div'); + messageEl.classList.add('info'); + + + var nickSpanEl = document.createElement('span'); + nickSpanEl.classList.add('nick'); + messageEl.appendChild(nickSpanEl); + + var nickLinkEl = document.createElement('a'); + nickLinkEl.textContent = '#'; + nickSpanEl.appendChild(nickLinkEl); + + var textEl = document.createElement('pre'); + textEl.style.fontSize = '4px'; + textEl.classList.add('text'); + textEl.innerHTML = args.text; + + messageEl.appendChild(textEl); + $('#messages').appendChild(messageEl); + + window.scrollTo(0, document.body.scrollHeight); } } @@ -447,7 +473,13 @@ function pushMessage(args) { if (args.trip) { var tripEl = document.createElement('span'); - tripEl.textContent = args.trip + " "; + + if (args.mod) { + tripEl.textContent = String.fromCodePoint(11088) + " " + args.trip + " "; + } else { + tripEl.textContent = args.trip + " "; + } + tripEl.classList.add('trip'); nickSpanEl.appendChild(tripEl); } @@ -456,6 +488,10 @@ function pushMessage(args) { var nickLinkEl = document.createElement('a'); nickLinkEl.textContent = args.nick; + if (args.color && /(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(args.color)) { + nickLinkEl.setAttribute('style', 'color:#' + args.color + ' !important'); + } + nickLinkEl.onclick = function () { insertAtCursor("@" + args.nick + " "); $('#chatinput').focus(); diff --git a/server/src/commands/admin/removemod.js b/server/src/commands/admin/removemod.js index 85d5fd6..e1ab7af 100644 --- a/server/src/commands/admin/removemod.js +++ b/server/src/commands/admin/removemod.js @@ -6,6 +6,7 @@ import { isAdmin, isModerator, levels, + getUserDetails, } from '../utility/_UAC'; // module main @@ -24,6 +25,16 @@ export async function run({ // find targets current connections const targetMod = server.findSockets({ trip: payload.trip }); if (targetMod.length !== 0) { + // build update notice with new privileges + const updateNotice = { + ...getUserDetails(targetMod[0]), + ...{ + cmd: 'updateUser', + uType: 'user', // @todo use legacyLevelToLabel from _LegacyFunctions.js + level: levels.default, + }, + }; + for (let i = 0, l = targetMod.length; i < l; i += 1) { // downgrade privilages targetMod[i].uType = 'user'; @@ -35,6 +46,14 @@ export async function run({ text: 'You are now a user.', channel: targetMod[i].channel, // @todo Multichannel }, targetMod[i]); + + // notify channel + server.broadcast({ + ...updateNotice, + ...{ + channel: targetMod[i].channel, + }, + }, { channel: targetMod[i].channel }); } } diff --git a/server/src/commands/core/changecolor.js b/server/src/commands/core/changecolor.js index 2801fcf..0eecdcd 100644 --- a/server/src/commands/core/changecolor.js +++ b/server/src/commands/core/changecolor.js @@ -2,12 +2,16 @@ Description: Allows calling client to change their nickname color */ +import { + getUserDetails, +} from '../utility/_UAC'; + // module support functions const verifyColor = (color) => /(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(color); // module main export async function run({ - core, server, socket, payload, + server, socket, payload, }) { const channel = socket.channel; @@ -35,9 +39,9 @@ export async function run({ } if (newColor === 'RESET') { - socket.color = false; + socket.color = false; // eslint-disable-line no-param-reassign } else { - socket.color = newColor; + socket.color = newColor; // eslint-disable-line no-param-reassign } // build update notice with new color @@ -46,7 +50,7 @@ export async function run({ ...{ cmd: 'updateUser', channel: socket.channel, // @todo Multichannel - } + }, }; // notify channel that the user has changed their name diff --git a/server/src/commands/core/changenick.js b/server/src/commands/core/changenick.js index 558a1b8..8bc3b6d 100644 --- a/server/src/commands/core/changenick.js +++ b/server/src/commands/core/changenick.js @@ -85,7 +85,7 @@ export async function run({ cmd: 'updateUser', nick: newNick, channel, // @todo Multichannel - } + }, }; // build join and leave notices for legacy clients @@ -114,7 +114,7 @@ export async function run({ server.send(joinNotice, peerList[i]); } else { // send update info - // @todo this should be sent to every channel the client is in + // @todo this should be sent to every channel the client is in (multichannel) server.send(updateNotice, peerList[i]); } } diff --git a/server/src/commands/core/join.js b/server/src/commands/core/join.js index 9332c8c..45e6446 100644 --- a/server/src/commands/core/join.js +++ b/server/src/commands/core/join.js @@ -76,7 +76,7 @@ export async function run({ // get trip and level const { trip, level } = getUserPerms(pass, core.config, channel); - + // store the user values const userInfo = { nick, @@ -122,7 +122,7 @@ export async function run({ // send join announcement and prep online set reply for (let i = 0, l = newPeerList.length; i < l; i += 1) { server.reply(joinAnnouncement, newPeerList[i]); - + nicks.push(newPeerList[i].nick); /* @legacy */ users.push({ ...{ diff --git a/server/src/commands/core/move.js b/server/src/commands/core/move.js index d6537c5..de6a4fb 100644 --- a/server/src/commands/core/move.js +++ b/server/src/commands/core/move.js @@ -3,8 +3,6 @@ @deprecated This module will be removed or replaced */ import { - verifyNickname, - getUserPerms, getUserDetails, } from '../utility/_UAC'; @@ -104,7 +102,7 @@ export async function run({ server, socket, payload }) { channel: payload.channel, isme: true, }, - ...getUserDetails(socket) + ...getUserDetails(socket), }); // reply with new user list diff --git a/server/src/commands/core/whisper.js b/server/src/commands/core/whisper.js index 6705cb7..25bc1b6 100644 --- a/server/src/commands/core/whisper.js +++ b/server/src/commands/core/whisper.js @@ -16,7 +16,6 @@ import { } from '../utility/_LegacyFunctions'; // module support functions - const parseText = (text) => { // verifies user input is text if (typeof text !== 'string') { diff --git a/server/src/commands/mod/dumb.js b/server/src/commands/mod/dumb.js index 5b66684..4625b58 100644 --- a/server/src/commands/mod/dumb.js +++ b/server/src/commands/mod/dumb.js @@ -9,12 +9,45 @@ import { isModerator, } from '../utility/_UAC'; +import { + findUser, +} from '../utility/_Channels'; import { Errors, } from '../utility/_Constants'; import { - findUser, -} from '../utility/_Channels'; + legacyInviteReply, + legacyWhisperReply, +} from '../utility/_LegacyFunctions'; + +// module support functions +/** + * Returns the channel that should be invited to. + * @param {any} channel + * @return {string} + */ +export function getChannel(channel = undefined) { + if (typeof channel === 'string') { + return channel; + } + return Math.random().toString(36).substr(2, 8); +} + +const parseText = (text) => { + // verifies user input is text + if (typeof text !== 'string') { + return false; + } + + let sanitizedText = text; + + // strip newlines from beginning and end + sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); + // replace 3+ newlines with just 2 newlines + sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n'); + + return sanitizedText; +}; // module constructor export function init(core) { @@ -101,24 +134,31 @@ export function chatCheck({ if (core.muzzledHashes[socket.hash]) { // build fake chat payload - const mutedPayload = { + const outgoingPayload = { cmd: 'chat', - nick: socket.nick, + nick: socket.nick, /* @legacy */ + uType: socket.uType, /* @legacy */ + userid: socket.userid, + channel: socket.channel, text: payload.text, - channel: socket.channel, // @todo Multichannel + level: socket.level, }; if (socket.trip) { - mutedPayload.trip = socket.trip; + outgoingPayload.trip = socket.trip; + } + + if (socket.color) { + outgoingPayload.color = socket.color; } // broadcast to any duplicate connections in channel - server.broadcast(mutedPayload, { channel: socket.channel, hash: socket.hash }); + server.broadcast(outgoingPayload, { channel: socket.channel, hash: socket.hash }); // broadcast to allies, if any if (core.muzzledHashes[socket.hash].allies) { server.broadcast( - mutedPayload, + outgoingPayload, { channel: socket.channel, nick: core.muzzledHashes[socket.hash].allies, @@ -140,24 +180,62 @@ export function chatCheck({ } // shadow-prevent all invites from muzzled users -export function inviteCheck({ core, socket, payload }) { +export function inviteCheck({ + core, server, socket, payload, +}) { if (core.muzzledHashes[socket.hash]) { - // @todo convert to protocol 2 - /* const nickValid = Invite.checkNickname(payload.nick); - if (nickValid !== null) { - server.reply({ - cmd: 'warn', // @todo Add numeric error code as `id` - text: nickValid, + // check for spam + if (server.police.frisk(socket.address, 2)) { + return server.reply({ + cmd: 'warn', + text: 'You are sending invites too fast. Wait a moment before trying again.', + id: Errors.Invite.RATELIMIT, + channel: socket.channel, // @todo Multichannel + }, socket); + } + + // verify user input + // if this is a legacy client add missing params to payload + if (socket.hcProtocol === 1) { + if (typeof socket.channel === 'undefined' || typeof payload.nick !== 'string') { + return true; + } + + payload.channel = socket.channel; // eslint-disable-line no-param-reassign + } else if (typeof payload.userid !== 'number' || typeof payload.channel !== 'string') { + return true; + } + + // @todo Verify this socket is part of payload.channel - multichannel patch + // find target user + const targetUser = findUser(server, payload); + if (!targetUser) { + return server.reply({ + cmd: 'warn', + text: 'Could not find user in that channel', + id: Errors.Global.UNKNOWN_USER, channel: socket.channel, // @todo Multichannel }, socket); - return false; } // generate common channel - const channel = Invite.getChannel(); + const channel = getChannel(payload.to); - // send fake reply - server.reply(Invite.createSuccessPayload(payload.nick, channel), socket); */ + // build invite + const outgoingPayload = { + cmd: 'invite', + channel: socket.channel, // @todo Multichannel + from: socket.userid, + to: targetUser.userid, + inviteChannel: channel, + }; + + // send invite notice to this client + if (socket.hcProtocol === 1) { + server.reply(legacyInviteReply(outgoingPayload, targetUser.nick), socket); + } else { + server.reply(outgoingPayload, socket); + } return false; } @@ -169,27 +247,56 @@ export function inviteCheck({ core, socket, payload }) { export function whisperCheck({ core, server, socket, payload, }) { - if (typeof payload.nick !== 'string') { - return false; - } - - if (typeof payload.text !== 'string') { - return false; - } - if (core.muzzledHashes[socket.hash]) { - const targetNick = payload.nick; + // if this is a legacy client add missing params to payload + if (socket.hcProtocol === 1) { + payload.channel = socket.channel; // eslint-disable-line no-param-reassign + } - server.reply({ - cmd: 'info', - type: 'whisper', - text: `You whispered to @${targetNick}: ${payload.text}`, + // verify user input + const text = parseText(payload.text); + + if (!text) { + // lets not send objects or empty text, yea? + return server.police.frisk(socket.address, 13); + } + + // check for spam + const score = text.length / 83 / 4; + if (server.police.frisk(socket.address, score)) { + return server.reply({ + cmd: 'warn', // @todo Add numeric error code as `id` + text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.', + channel: socket.channel, // @todo Multichannel + }, socket); + } + + const targetUser = findUser(server, payload); + if (!targetUser) { + return server.reply({ + cmd: 'warn', + text: 'Could not find user in that channel', + id: Errors.Global.UNKNOWN_USER, + channel: socket.channel, // @todo Multichannel + }, socket); + } + + const outgoingPayload = { + cmd: 'whisper', channel: socket.channel, // @todo Multichannel - }, socket); + from: socket.userid, + to: targetUser.userid, + text, + }; - // blanket "spam" protection, may expose the ratelimiting lines from - // `chat` and use that, @todo one day #lazydev - server.police.frisk(socket.address, 9); + // send invite notice to this client + if (socket.hcProtocol === 1) { + server.reply(legacyWhisperReply(outgoingPayload, targetUser.nick), socket); + } else { + server.reply(outgoingPayload, socket); + } + + targetUser.whisperReply = socket.nick; return false; } diff --git a/server/src/commands/mod/forcecolor.js b/server/src/commands/mod/forcecolor.js new file mode 100644 index 0000000..42eb2b8 --- /dev/null +++ b/server/src/commands/mod/forcecolor.js @@ -0,0 +1,162 @@ +/* + Description: Forces a change on the target socket's nick color +*/ + +import { + isModerator, + getUserDetails, +} from '../utility/_UAC'; +import { + Errors, +} from '../utility/_Constants'; +import { + findUser, +} from '../utility/_Channels'; + +const verifyColor = (color) => /(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(color); + +// module main +export async function run({ + 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); + } + + const channel = socket.channel; + if (typeof payload.channel === 'undefined') { + payload.channel = channel; + } + + // check user input + if (typeof payload.nick !== 'string') { + return true; + } + + if (typeof payload.color !== 'string') { + return true; + } + + // make sure requested nickname meets standards + const newColor = payload.color.trim().toUpperCase().replace(/#/g, ''); + if (newColor !== 'RESET' && !verifyColor(newColor)) { + return server.reply({ + cmd: 'warn', + text: 'Invalid color! Color must be in hex value', + channel, // @todo Multichannel + }, socket); + } + + // find target user + const targetUser = findUser(server, payload); + if (!targetUser) { + return server.reply({ + cmd: 'warn', + text: 'Could not find user in that channel', + id: Errors.Global.UNKNOWN_USER, + channel: socket.channel, // @todo Multichannel + }, socket); + } + + // TODO: Change this uType to use level / uac + // i guess coloring mods or admins isn't the best idea? + if (targetUser.uType !== 'user') { + return true; + } + + if (newColor === 'RESET') { + targetUser.color = false; + } else { + targetUser.color = newColor; + } + + // build update notice with new color + const updateNotice = { + ...getUserDetails(targetUser), + ...{ + cmd: 'updateUser', + channel: socket.channel, // @todo Multichannel + }, + }; + + // notify channel that the user has changed their name + // @todo this should be sent to every channel the user is in (multichannel) + server.broadcast(updateNotice, { channel: socket.channel }); + + // mod perks + // TODO: Change this uType to use level / uac + if (socket.uType !== 'user') { + if (typeof server.police.records[socket.address] !== 'undefined') { + server.police.records[socket.address].score = -50; + } + } + + return true; +} + +// module hook functions +export function initHooks(server) { + server.registerHook('in', 'chat', this.colorCheck.bind(this), 20); +} + +// hooks chat commands checking for /whisper +export function colorCheck({ + core, server, socket, payload, +}) { + if (typeof payload.text !== 'string') { + return false; + } + + if (payload.text.startsWith('/forcecolor ')) { + const input = payload.text.split(' '); + + // If there is no nickname target parameter + if (input[1] === undefined) { + server.reply({ + cmd: 'warn', + text: 'Refer to `/help forcecolor` for instructions on how to use this command.', + channel: socket.channel, // @todo Multichannel + }, socket); + + return false; + } + + if (input[2] === undefined) { + server.reply({ + cmd: 'warn', + text: 'Refer to `/help forcecolor` for instructions on how to use this command.', + channel: socket.channel, // @todo Multichannel + }, socket); + + return false; + } + + const target = input[1].replace(/@/g, ''); + + this.run({ + core, + server, + socket, + payload: { + cmd: 'forcecolor', + nick: target, + color: input[2], + }, + }); + + return false; + } + + return payload; +} + +// module meta +export const requiredData = ['nick', 'color']; +export const info = { + name: 'forcecolor', + description: 'Forces a user nick to become a certain color', + usage: ` + API: { cmd: 'forcecolor', nick: '', color: '' } +Text: /forcecolor `, +}; diff --git a/server/src/commands/mod/moveuser.js b/server/src/commands/mod/moveuser.js index a616620..edae2aa 100644 --- a/server/src/commands/mod/moveuser.js +++ b/server/src/commands/mod/moveuser.js @@ -78,7 +78,6 @@ export async function run({ server, socket, payload }) { } } - const newPeerList = server.findSockets({ channel: payload.channel }); const moveAnnouncement = { ...getUserDetails(badClient), @@ -95,7 +94,7 @@ export async function run({ server, socket, payload }) { nicks.push(newPeerList[i].nick); /* @legacy */ users.push({ ...{ - channel, + channel: payload.channel, isme: false, }, ...getUserDetails(newPeerList[i]), @@ -114,7 +113,7 @@ export async function run({ server, socket, payload }) { cmd: 'onlineSet', nicks, /* @legacy */ users, - channel, // @todo Multichannel (?) + channel: payload.channel, // @todo Multichannel (?) }, badClient); badClient.channel = payload.channel; diff --git a/server/src/commands/utility/_LegacyFunctions.js b/server/src/commands/utility/_LegacyFunctions.js index 7c27b7b..4fa9891 100644 --- a/server/src/commands/utility/_LegacyFunctions.js +++ b/server/src/commands/utility/_LegacyFunctions.js @@ -61,7 +61,7 @@ export function legacyLevelToLabel(level) { * @param {string} nick Sender nick * @return {object} */ - export function legacyInviteOut(payload, nick) { +export function legacyInviteOut(payload, nick) { return { ...payload, ...{ @@ -99,7 +99,7 @@ export function legacyInviteReply(payload, nick) { * @param {string} nick Sender nick * @return {object} */ - export function legacyWhisperOut(payload, from) { +export function legacyWhisperOut(payload, from) { return { ...payload, ...{