// Copyright 2018-2022 Campbell Crowley. All rights reserved. // Author: Campbell Crowley (web@campbellcrowley.com) const http = require('http'); const auth = require('../../auth.js'); const socketIo = require('socket.io'); const MessageMaker = require('../lib/MessageMaker.js'); require('../subModule.js').extend(WebSettings); // Extends the SubModule class. /** * @classdesc Manages changing settings for the bot from a website. * @class * @augments SubModule */ function WebSettings() { const self = this; /** @inheritdoc */ this.myName = 'WebSettings'; /** @inheritdoc */ this.initialize = function() { if (self.common.isSlave) { startClient(); } else { app.listen(self.common.isRelease ? 8020 : 8021, '127.0.0.1'); } setTimeout(updateModuleReferences, 100); self.command.addEventListener('settingsChanged', handleSettingsChanged); self.command.addEventListener('settingsReset', handleSettingsReset); }; /** @inheritdoc */ this.unloadable = function() { return getNumClients() == 0; }; /** @inheritdoc */ this.shutdown = function() { if (io) io.close(); if (ioClient) { ioClient.close(); ioClient = null; } if (app) app.close(); if (cmdScheduler) { cmdScheduler.removeListener('shutdown', handleShutdown); cmdScheduler.removeListener('commandRegistered', handleCommandRegistered); cmdScheduler.removeListener('commandCancelled', handleCommandCancelled); self.command.removeEventListener( 'settingsChanged', handleSettingsChanged); self.command.removeEventListener('settingsReset', handleSettingsReset); } }; let ioClient; let app; let io; if (!this.common.isSlave) { app = http.createServer(handler); io = socketIo(app, {path: '/socket.io/'}); io.on('connection', socketConnection); app.on('error', (err) => { if (io) io.close(); if (app) app.close(); if (err.code === 'EADDRINUSE') { this.warn( 'Settings failed to bind to port because it is in use. (' + err.port + ')'); if (!this.common.isMaster) { self.log( 'Restarting into client mode due to server already bound ' + 'to port.'); startClient(); } } else { console.error( 'Settings failed to bind to port for unknown reason.', err); } }); } /** * @description Function calls handlers for requested commands. * @typedef WebSettings~SocketFunction * @type {Function} * * @param {WebUserData} userData The user data of the user performing the * request. * @param {socketIo~Socket} socket The socket connection firing the command. * Not necessarily the socket that will reply to the end client. * @param {...*} args Additional function-specific arguments. * @param {WebSettings~basicCB} [cb] Callback that fires once requested action * is complete or has failed. Client may not pass a callback. */ /** * Stores the current reference to the CmdScheduling subModule. Null if it * doesn't exist. * * @private * @type {?CmdScheduling} */ let cmdScheduler; /** * Stores the current reference to the RaidBlock subModule. Null if it doesn't * exist. * * @private * @type {?RaidBlock} */ let raidBlock; /** * Update the references to the aplicable subModules. * * @private */ function updateModuleReferences() { if (!self.initialized) return; if (!cmdScheduler || !cmdScheduler.initialized) { cmdScheduler = self.bot.getSubmodule('./cmdScheduling.js'); if (!cmdScheduler || !cmdScheduler.initialized) { cmdScheduler = null; setTimeout(updateModuleReferences, 100); } else { cmdScheduler.on('shutdown', handleShutdown); cmdScheduler.on('commandRegistered', handleCommandRegistered); cmdScheduler.on('commandCancelled', handleCommandCancelled); } } if (!raidBlock || !raidBlock.initialized) { raidBlock = self.bot.getSubmodule('./raidBlock.js'); if (!raidBlock || !raidBlock.initialized) { raidBlock = null; if (cmdScheduler && cmdScheduler.initialized) { setTimeout(updateModuleReferences, 100); } } else { raidBlock.on('shutdown', handleRaidShutdown); raidBlock.on('lockdown', handleLockdown); raidBlock.on('action', handleRaidAction); } } } /** * Handle CmdScheduling shutting down. * * @private * @listens CmdScheduling#shutdown */ function handleShutdown() { if (cmdScheduler) { cmdScheduler.removeListener('shutdown', handleShutdown); cmdScheduler.removeListener('commandRegistered', handleCommandRegistered); cmdScheduler.removeListener('commandCancelled', handleCommandCancelled); } cmdScheduler = null; if (!self.initialized) return; setTimeout(updateModuleReferences, 100); } /** * Handle RaidBlock shutting down. * * @private * @listens RaidBlock#shutdown */ function handleRaidShutdown() { if (raidBlock) { raidBlock.removeListener('shutdown', handleRaidShutdown); raidBlock.removeListener('lockdown', handleLockdown); raidBlock.removeListener('action', handleRaidAction); } raidBlock = null; if (!self.initialized) return; setTimeout(updateModuleReferences, 100); } /** * Handle new CmdScheduling.ScheduledCommand being registered. * * @private * @listens CmdScheduling#commandRegistered * * @param {CmdScheduling.ScheduledCommand} cmd The command that was scheduled. * @param {string|number} gId The guild ID of which the command was scheduled * in. */ function handleCommandRegistered(cmd, gId) { const toSend = { id: cmd.id, channel: cmd.channelId, cmd: cmd.cmd, repeatDelay: cmd.repeatDelay, time: cmd.time, member: makeMember(cmd.member), }; guildBroadcast(gId, 'commandRegistered', toSend, gId); } /** * Handle a CmdScheduling.ScheduledCommand being canceled. * * @private * @listens CmdScheduling#commandCancelled * @param {string} cmdId The ID of the command that was cancelled. * @param {string|number} gId The ID of the guild the command was cancelled * in. */ function handleCommandCancelled(cmdId, gId) { guildBroadcast(gId, 'commandCancelled', cmdId, gId); } /** * Handle Command~CommandSetting value changed. * * @private * @listens Command.events#settingsChanged * @see {@link Command~CommandSetting.set} * * @param {?string} gId The ID of the guild this setting was changed in, or * null of not specific to a single guild. * @param {string} value Value of setting. * @param {string} type Type of value. * @param {string} id Setting id. * @param {string} [id2] Second setting id. */ function handleSettingsChanged(gId, value, type, id, id2) { guildBroadcast(gId, 'settingsChanged', gId, value, type, id, id2); } /** * Handle Command~CommandSetting was deleted or reset in a guild. * * @private * @listens Command.events#settingsReset * * @param {string} gId The ID of the guild in which the settings were reset. */ function handleSettingsReset(gId) { guildBroadcast(gId, 'settingsReset', gId); } /** * Handle a guild going on lockdown. * * @private * @listens RaidBlock#lockdown * * @param {{settings: RaidBlock~RaidSettings, id: string}} event Event * information. */ function handleLockdown(event) { guildBroadcast(event.id, 'lockdown', event.id, event.settings); } /** * Handle a guild lockdown action being performed. * * @private * @listens RaidBlock#action * * @param {{action: string, user: Discord~User}} event Event * information. */ function handleRaidAction(event) { guildBroadcast( event.id, 'raidAction', event.id, event.action, event.user.id); } /** * Start a socketio client connection to the primary running server. * * @private */ function startClient() { const client = require('socket.io-client'); if (self.common.isSlave) { const host = self.common.masterHost; const port = host.host === 'localhost' ? (self.common.isRelease ? 8020 : 8021) : host.port; ioClient = client(`${host.protocol}//${host.host}:${port}`, { path: `${host.path}child/control/`, }); } else { ioClient = client( self.common.isRelease ? 'http://localhost:8020' : 'http://localhost:8021', {path: '/socket.io/control/'}); } clientSocketConnection(ioClient); } /** * Handler for all http requests. Should never be called. * * @private * @param {http.IncomingMessage} req The client's request. * @param {http.ServerResponse} res Our response to the client. */ function handler(req, res) { res.writeHead(418); res.end('TEAPOT'); } /** * Map of all currently connected sockets. * * @private * @type {object.<Socket>} */ const sockets = {}; /** * Returns the number of connected clients that are not siblings. * * @private * @returns {number} Number of sockets. */ function getNumClients() { return Object.keys(sockets).length - Object.keys(siblingSockets).length; } /** * Map of all sockets connected that are siblings. * * @private * @type {object.<Socket>} */ const siblingSockets = {}; /** * Handler for a new socket connecting. * * @private * @param {socketIo~Socket} socket The socket.io socket that connected. */ function socketConnection(socket) { // x-forwarded-for is trusted because the last process this jumps through is // our local proxy. const ipName = self.common.getIPName( socket.handshake.headers['x-forwarded-for'] || socket.handshake.address); self.common.log( 'Socket connected Settings (' + Object.keys(sockets).length + '): ' + ipName, socket.id); sockets[socket.id] = socket; socket.emit('time', Date.now()); // @TODO: Replace this authentication with gpg key-pairs; socket.on('vaderIAmYourSon', (verification, cb) => { if (verification === auth.webSettingsSiblingVerification) { siblingSockets[socket.id] = socket; cb(auth.webSettingsSiblingVerificationResponse); socket.on('_guildBroadcast', (gId, ...args) => { for (const i in sockets) { if (sockets[i] && sockets[i].cachedGuilds && sockets[i].cachedGuilds.includes(gId)) { sockets[i].emit(...args); } } }); } else { self.common.error('Client failed to authenticate as child.', socket.id); } }); socket.on('fetchGuilds', (...args) => handle(fetchGuilds, args, false)); socket.on('fetchGuild', (...args) => handle(self.fetchGuild, args)); socket.on('fetchMember', (...args) => handle(self.fetchMember, args)); socket.on('fetchChannel', (...args) => handle(self.fetchChannel, args)); socket.on('fetchSettings', (...args) => handle(self.fetchSettings, args)); socket.on( 'fetchRaidSettings', (...args) => handle(self.fetchRaidSettings, args)); socket.on( 'fetchModLogSettings', (...args) => handle(self.fetchModLogSettings, args)); socket.on( 'fetchCommandSettings', (...args) => handle(self.fetchCommandSettings, args)); socket.on( 'fetchScheduledCommands', (...args) => handle(self.fetchScheduledCommands, args)); socket.on( 'fetchGuildScheduledCommands', (...args) => handle(self.fetchGuildScheduledCommands, args)); socket.on( 'cancelScheduledCommand', (...args) => handle(self.cancelScheduledCommand, args)); socket.on( 'registerScheduledCommand', (...args) => handle(self.registerScheduledCommand, args)); socket.on('changePrefix', (...args) => handle(self.changePrefix, args)); socket.on( 'changeRaidSetting', (...args) => handle(self.changeRaidSetting, args)); socket.on( 'changeModLogSetting', (...args) => handle(self.changeModLogSetting, args)); socket.on( 'changeCommandSetting', (...args) => handle(self.changeCommandSetting, args)); /** * Calls the functions with added arguments, and copies the request to all * sibling clients. * * @private * @param {WebSettings~SocketFunction} func The function to call. * @param {Array.<*>} args Array of arguments to send to function. * @param {boolean} [forward=true] Forward this request directly to all * siblings. */ function handle(func, args, forward = true) { const noLog = ['fetchMember', 'fetchChannel']; if (!noLog.includes(func.name.toString())) { const logArgs = args.map((el) => { if (typeof el === 'function') { return (el.name || 'cb') + '()'; } else { return el; } }); self.common.logDebug(`${func.name}(${logArgs.join(',')})`, socket.id); } let cb; if (typeof args[args.length - 1] === 'function') { const origCB = args[args.length - 1]; let fired = false; cb = function(...args) { if (fired) { self.warn( 'Attempting to fire callback a second time! (' + func.name + ')'); } origCB(...args); fired = true; }; args[args.length - 1] = cb; } func.apply(func, [args[0], socket].concat(args.slice(1))); if (typeof cb === 'function') { args[args.length - 1] = {_function: true}; } if (forward) { Object.entries(siblingSockets).forEach((s) => { s[1].emit( 'forwardedRequest', args[0], socket.id, func.name, args.slice(1), (res) => { if (res._forward) socket.emit(...res.data); if (res._callback && typeof cb === 'function') { cb(...res.data); } }); }); } } socket.on('disconnect', (reason) => { self.common.log( 'Socket disconnected Settings (' + (Object.keys(sockets).length - 1) + ')(' + reason + '): ' + ipName, socket.id); if (siblingSockets[socket.id]) delete siblingSockets[socket.id]; delete sockets[socket.id]; }); } /** * Handler for connecting as a client to the server. * * @private * @param {socketIo~Socket} socket The socket.io socket that connected. */ function clientSocketConnection(socket) { let authenticated = false; socket.on('connect', () => { socket.emit( 'vaderIAmYourSon', auth.webSettingsSiblingVerification, (res) => { self.common.log('Sibling authenticated successfully.'); authenticated = res === auth.webSettingsSiblingVerificationResponse; }); }); socket.on('fetchGuilds', (userData, id, cb) => { fetchGuilds(userData, {id: id}, cb); }); socket.on('forwardedRequest', (userData, sId, func, args, cb) => { if (!authenticated) return; const fakeSocket = { fake: true, emit: function(...args) { if (typeof cb == 'function') cb({_forward: true, data: args}); }, id: sId, }; if (args[args.length - 1] && args[args.length - 1]._function) { args[args.length - 1] = function(...a) { if (typeof cb === 'function') cb({_callback: true, data: a}); }; } if (!self[func]) { self.common.error(func + ': is not a function.', socket.id); } else { self[func].apply(self[func], [userData, fakeSocket].concat(args)); } }); const error = function(...args) { console.error(...args); }; socket.on('connect_error', error); socket.on('connect_timeout', error); socket.on('reconnect_error', error); socket.on('reconnect_failed', error); socket.on('error', error); } /** * Broadcast a message to all relevant clients. * * @private * @param {string} gId Guild ID to broadcast message for. * @param {string} event The name of the event to broadcast. * @param {*} args Data to send in broadcast. */ function guildBroadcast(gId, event, ...args) { const keys = Object.keys(sockets); for (const i in keys) { if (!sockets[keys[i]].cachedGuilds) continue; if (sockets[keys[i]].cachedGuilds.find((g) => g === gId)) { sockets[keys[i]].emit(event, gId, ...args); } } if (ioClient) { ioClient.emit('_guildBroadcast', gId, event, gId, ...args); } } /** * Send a message to the given socket informing the client that the command * they attempted failed due to insufficient permission. * * @private * @param {Socket} socket The socket.io socket to reply on. * @param {string} cmd THe command the client attempted. */ function replyNoPerm(socket, cmd) { self.common.logDebug( 'Attempted ' + cmd + ' without permission.', socket.id); socket.emit( 'message', 'Failed to run command "' + cmd + '" because you don\'t have permission for this.'); } /** * Checks if the current shard is responsible for the requested guild. * * @private * @param {number|string} gId The guild id to check. * @returns {boolean} True if this shard has this guild. */ function checkMyGuild(gId) { const g = self.client && self.client.guilds.resolve(gId); return (g && true) || false; } /** * Check that the given user has permission to manage the games in the given * guild. * * @private * @param {UserData} userData The user to check. * @param {string} gId The guild id to check against. * @param {?string} cId The channel id to check against. * @param {string} cmd The command being attempted. * @returns {boolean} Whether the user has permission or not to manage the * hungry games in the given guild. */ function checkPerm(userData, gId, cId, cmd) { if (!userData) return false; const msg = makeMessage(userData.id, gId, cId, cmd); if (!msg) return false; if (userData.id == self.common.spikeyId) return true; if (self.command.validate(null, makeMessage(userData.id, gId, null, cmd))) { return false; } return true; } /** * Check that the given user has permission to see and send messages in the * given channel, as well as manage the games in the given guild. * * @private * @param {UserData} userData The user to check. * @param {string} gId The guild id of the guild that contains the channel. * @param {string} cId The channel id to check against. * @returns {boolean} Whether the user has permission or not to manage the * hungry games in the given guild and has permission to send messages in the * given channel. */ function checkChannelPerm(userData, gId, cId) { if (!userData) return false; const g = self.client && self.client.guilds.resolve(gId); if (!g) return false; if (userData.id == self.common.spikeyId) return true; const m = g.members.resolve(userData.id); if (!m) return false; const channel = g.channels.resolve(cId); if (!channel) return false; const perms = channel.permissionsFor(m); if (!perms.has(self.Discord.PermissionsBitField.Flags.ViewChannel)) { return false; } if (!perms.has(self.Discord.PermissionsBitField.Flags.SendMessages)) { return false; } return true; } /** * Strips a Discord~GuildMember to only the necessary data that a client will * need. * * @private * @param {Discord~GuildMember} m The guild member to strip the data from. * @returns {object} The minimal member. */ function makeMember(m) { if (!m) return null; if (typeof m !== 'object') { m = { roles: { cache: { array: function() { return []; }, }, }, guild: {}, permissions: {bitfield: 0}, user: self.client.users.resolve(m), }; } return { nickname: m.nickname, roles: [...m.roles.cache.values()], color: m.displayColor, guild: {id: m.guild.id}, user: { username: m.user.username, tag: m.user.tag, discriminator: m.user.discriminator, avatarURL: m.user.displayAvatarURL(), id: m.user.id, bot: m.user.bot, }, joinedTimestamp: m.joinedTimestamp, }; } /** * Forms a Discord~Message similar object from given IDs. * * @private * @param {string} uId The id of the user who wrote this message. * @param {string} gId The id of the guild this message is in. * @param {?string} cId The id of the channel this message was 'sent' in. * @param {?string} msg The message content. * @returns {MessageMaker} The created message-like object. */ function makeMessage(uId, gId, cId, msg) { const message = new MessageMaker(self, uId, gId, cId, msg); return message.guild ? message : null; } /** * Basic callback with single argument. The argument is null if there is no * error, or a string if there was an error. * * @callback WebSettings~basicCB * * @param {?string} err The error response. * @param {*} res Response data if no error. */ /** * Fetch all relevant data for all mutual guilds with the user and send it to * the user. * * @private * @type {WebSettings~SocketFunction} * @param {WebUserData} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {Function} [cb] Callback that fires once the requested action is * complete, or has failed. */ function fetchGuilds(userData, socket, cb) { if (!userData) { self.common.error('Fetch Guilds without userData', 'WebSettings'); if (typeof cb === 'function') cb('Not signed in', null); return; } else if (userData.apiRequest) { // Disabled for API requests due to the possible issue with performance // fetching list of guilds. return; } const numReplies = (Object.entries(siblingSockets).length || 0); let replied = 0; const guildBuffer = {}; let done; if (typeof cb === 'function') { done = cb; } else { /** * The callback for each response with the requested data. Replies to the * user once all requests have replied. * * @private * @param {string|object} guilds Either the guild data to send to the * user, or 'guilds' if this is a reply from a sibling client. * @param {?string} [err] The error that occurred, or null if no error. * @param {object} [response] The guild data if `guilds` equals 'guilds'. */ done = function(guilds, err, response) { if (guilds === 'guilds') { if (err) { guilds = null; } else { guilds = response; } } for (let i = 0; guilds && i < guilds.length; i++) { guildBuffer[guilds[i].id] = guilds[i]; } replied++; if (replied > numReplies) { if (typeof cb === 'function') cb(guildBuffer); socket.emit('guilds', null, guildBuffer); socket.cachedGuilds = Object.keys(guildBuffer || {}); } }; } Object.values(siblingSockets).forEach((obj) => { obj.emit('fetchGuilds', userData, socket.id, done); }); if (self.common.isMaster) { done([]); return; } try { let guilds = []; if (userData.guilds && userData.guilds.length > 0) { userData.guilds.forEach((el) => { const g = self.client && self.client.guilds.resolve(el.id); if (!g) return; guilds.push(g); }); } else { guilds = self.client && [...self.client.guilds.cache .filter((obj) => obj.members.resolve(userData.id)) .values()]; } const strippedGuilds = stripGuilds(guilds, userData); socket.cachedGuilds = strippedGuilds.map((g) => g.id); done(strippedGuilds); } catch (err) { self.error(err); // socket.emit('guilds', 'Failed', null); done(); } } this.fetchGuilds = fetchGuilds; /** * Strip a Discord~Guild to the basic information the client will need. * * @private * @param {Discord~Guild[]} guilds The array of guilds to strip. * @param {object} userData The current user's session data. * @returns {Array<object>} The stripped guilds. */ function stripGuilds(guilds, userData) { return guilds.map((g) => { const member = g.members.resolve(userData.id); const newG = {}; newG.iconURL = g.iconURL(); newG.name = g.name; newG.id = g.id; newG.ownerId = g.ownerId; newG.members = g.members.cache.map((m) => m.id); newG.channels = g.channels.cache .filter((c) => { const perms = c.permissionsFor(member); return userData.id == self.common.spikeyId || (perms && perms.has( self.Discord.PermissionsBitField.Flags.ViewChannel)); }) .map((c) => c.id); newG.myself = makeMember(member || userData.id); return newG; }); } /** * Fetch a single guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string|number} gId The ID of the guild that was requested. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete, or has failed. */ this.fetchGuild = function fetchGuild(userData, socket, gId, cb) { if (!userData) { self.common.error('Fetch Guild without userData', socket.id); if (typeof cb === 'function') cb('SIGNED_OUT'); return; } if (typeof cb !== 'function') { self.common.logWarning( 'Fetch Guild attempted without callback', socket.id); return; } const guild = self.client && self.client.guilds.resolve(gId); if (!guild) return; if (userData.id != self.common.spikeyId && !guild.members.resolve(userData.id)) { cb(null); return; } cb(null, stripGuilds([guild], userData)[0]); }; /** * Fetch data about a member of a guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The guild id to look at. * @param {number|string} mId The member's id to lookup. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete, or has failed. */ this.fetchMember = function fetchMember(userData, socket, gId, mId, cb) { if (typeof cb !== 'function') return; if (!checkPerm(userData, gId, null, 'players')) return; const g = self.client && self.client.guilds.resolve(gId); if (!g) return; const m = g.members.resolve(mId); if (!m) { cb('No Member'); return; } const finalMember = makeMember(m); cb(null, finalMember); }; /** * Client has requested data for a specific channel. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {number|string} gId The ID of the Discord guild where the channel * is. * @param {number|string} cId The ID of the Discord channel to fetch. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete and has data, or has failed. */ this.fetchChannel = function fetchChannel(userData, socket, gId, cId, cb) { if (!checkMyGuild(gId)) return; if (typeof cb !== 'function') cb = function() {}; if (!checkChannelPerm(userData, gId, cId)) { replyNoPerm(socket, 'fetchChannel'); cb('NO_PERM'); return; } const c = self.client.channels.resolve(cId); const m = self.client.guilds.resolve(gId).members.resolve(userData.id); const perms = c.permissionsFor(m); const stripped = { id: c.id, permissions: perms, name: c.name, position: c.position, type: c.type, }; if (c.parent) { stripped.parent = {position: c.parent.position}; } cb(null, stripped); }; /** * Client has requested all settings for all guilds for the connected user. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete and has data, or has failed. */ this.fetchSettings = function fetchSettings(userData, socket, cb) { if (!userData) { if (typeof cb === 'function') cb('Not signed in.', null); return; } let guilds = []; if (userData.guilds && userData.guilds.length > 0) { userData.guilds.forEach((el) => { const g = self.client && self.client.guilds.resolve(el.id); if (!g) return; guilds.push(g); }); } else { guilds = self.client.guilds.cache.filter( (obj) => userData.id == self.common.spikeyId || obj.members.resolve(userData.id)); } const cmdDefaults = self.command.getDefaultSettings(); const modLog = self.bot.getSubmodule('./modLog.js'); const settings = guilds.map((g) => { return { guild: g.id, prefix: self.bot.getPrefix(g), commandSettings: self.command.getUserSettings(g.id), commandDefaults: cmdDefaults, raidSettings: raidBlock && raidBlock.getSettings(g.id) || null, modLogSettings: modLog && modLog.getSettings(g.id) || null, }; }); if (!socket.fake && typeof cb === 'function') { cb(null, settings); } else { socket.emit('settings', null, settings); } }; /** * Client has requested settings specific to raids for single guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} gId The guild ID to fetch the settings for. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete and has data, or has failed. */ this.fetchRaidSettings = function fetchRaidSettings( userData, socket, gId, cb) { if (!checkMyGuild(gId)) return; if (typeof cb !== 'function') cb = function() {}; if (!userData) { cb('Not signed in.', null); return; } if (userData.id != self.common.spikeyId) { const guild = self.client.guilds.resolve(gId); const member = guild.members.resolve(userData.id); if (!member) { cb('NO_PERM'); return; } } if (!raidBlock) { cb('Internal Server Error'); return; } cb(null, raidBlock.getSettings(gId)); }; /** * Client has requested settings specific to ModLog for single guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} gId The guild ID to fetch the settings for. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete and has data, or has failed. */ this.fetchModLogSettings = function fetchModLogSettings( userData, socket, gId, cb) { if (!checkMyGuild(gId)) return; if (typeof cb !== 'function') cb = function() {}; if (!userData) { cb('Not signed in.', null); return; } if (userData.id != self.common.spikeyId) { const guild = self.client.guilds.resolve(gId); const member = guild.members.resolve(userData.id); if (!member) { cb('NO_PERM'); return; } } const modLog = self.bot.getSubmodule('./modLog.js'); if (!modLog) { cb('Internal Server Error'); return; } cb(null, modLog.getSettings(gId)); }; /** * Client has requested settings specific to a single command in a single * guild. This only supplies user settings, if values are default, this will * reply with null. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} gId The guild ID to fetch the settings for. * @param {?string} cmd The name of the command to fetch the setting for, or * null to fetch all settings. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete and has data, or has failed. */ this.fetchCommandSettings = function fetchCommandSettings( userData, socket, gId, cmd, cb) { if (!checkMyGuild(gId)) return; if (typeof cb !== 'function') cb = function() {}; if (!userData) { cb('Not signed in.', null); return; } if (userData.id != self.common.spikeyId) { const guild = self.client.guilds.resolve(gId); const member = guild.members.resolve(userData.id); if (!member) { cb('NO_PERM'); return; } } let settings = self.command.getUserSettings(gId); if (cmd) { const command = self.command.find(cmd); if (!command) { settings = null; } else { settings = settings[command.getFullName()]; } } cb(null, settings); }; /** * Client has requested all scheduled commands for the connected user. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete and has data, or has failed. */ this.fetchScheduledCommands = function fetchScheduledCommands( userData, socket, cb) { if (self.common.isMaster) return; if (!userData) { if (!socket.fake && typeof cb === 'function') cb('Not signed in.', null); return; } let guilds = userData.guilds; if (guilds) { guilds.map((el) => self.client.guilds.resolve(el.id)); } else { guilds = self.client.guilds.cache.filter( (obj) => obj.members.resolve(userData.id)); } const sCmds = {}; updateModuleReferences(); if (!cmdScheduler) { self.warn('Failed to get reference to CmdScheduler!'); return; } guilds.forEach((g) => { if (!g) return; const list = cmdScheduler.getScheduledCommandsInGuild(g.id); if (list && list.length > 0) { sCmds[g.id] = list.map((el) => { return { id: el.id, channel: el.channel.id, cmd: el.cmd, repeatDelay: el.repeatDelay, time: el.time, member: makeMember(el.member), }; }); } }); if (!socket.fake && typeof cb === 'function') { cb(null, sCmds); } else { socket.emit('scheduledCmds', null, sCmds); } }; /** * Client has requested scheduled commands for a guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string} gId The guild ID to fetch. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete and has data, or has failed. */ this.fetchGuildScheduledCommands = function fetchGuildScheduledCommands( userData, socket, gId, cb) { if (!checkMyGuild(gId)) return; if (typeof cb !== 'function') cb = function() {}; if (!userData) { cb('Not signed in.', null); return; } if (userData.id != self.common.spikeyId) { const guild = self.client.guilds.resolve(gId); const member = guild.members.resolve(userData.id); if (!member) { cb('NO_PERM'); return; } } updateModuleReferences(); if (!cmdScheduler) { self.warn('Failed to get reference to CmdScheduler!'); return; } const list = cmdScheduler.getScheduledCommandsInGuild(gId); let sCmds; if (list && list.length > 0) { sCmds = list.map((el) => { return { id: el.id, channel: el.channel.id, cmd: el.cmd, repeatDelay: el.repeatDelay, time: el.time, member: makeMember(el.member), }; }); } cb(null, sCmds); }; /** * Client has requested that a scheduled command be cancelled. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string|number} gId The id of the guild of which to cancel the * command. * @param {string} cmdId The ID of the command to cancel. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete, or has failed. */ this.cancelScheduledCommand = function cancelScheduledCommand( userData, socket, gId, cmdId, cb) { if (typeof cb !== 'function') cb = function() {}; if (!checkPerm(userData, gId, null, 'schedule')) { if (!checkMyGuild(gId)) return; replyNoPerm(socket, 'cancelScheduledCommand'); cb('Forbidden'); return; } updateModuleReferences(); cmdScheduler.cancelCmd(gId, cmdId); cb(null); }; /** * @description Client has created a new scheduled command. * @see {@link CmdScheduling~ScheduledCommand} * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string|number} gId The id of the guild of which to add the command. * @param {object} cmd The command data of which to make into a * scheduled command and register. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete, or has failed. */ this.registerScheduledCommand = function registerScheduledCommand( userData, socket, gId, cmd, cb) { if (typeof cb !== 'function') cb = function() {}; if (!checkMyGuild(gId)) return; if (!checkPerm(userData, gId, cmd && cmd.channel, 'schedule')) { replyNoPerm(socket, 'registerScheduledCommand'); cb('Forbidden'); return; } if (!cmd || typeof cmd !== 'object') { cb('Invalid Data'); return; } if (!cmd.time || cmd.time < Date.now()) { cb('Time cannot be in past.'); return; } updateModuleReferences(); if (cmd.repeatDelay && cmd.repeatDelay < cmdScheduler.minRepeatDelay) { cb('Repeat time is too soon.'); return; } let cId = self.client.channels.resolve(cmd.channel); if (!cId) { cb('Invalid Channel'); return; } cId = cId.id; if (typeof cmd.cmd !== 'string') { cb('Invalid Command'); return; } const msg = makeMessage(userData.id, gId, cId, cmd.cmd); if (!msg) { cb('Invalid Member'); return; } const invalid = self.command.validate(cmd.cmd.split(/\s/)[0], msg); if (invalid) { cb('Invalid Command'); return; } const prefix = self.bot.getPrefix(gId); if (!cmd.cmd.startsWith(prefix)) { cmd.cmd = prefix + cmd.cmd; } const single = self.command.find(cmd.cmd, {prefix: prefix}); if (!single) { cb('Invalid Command'); return; } if (single.getFullName() === self.command.find('sch').getFullName()) { cb('Invalid Command'); return; } const newCmd = new cmdScheduler.ScheduledCommand({ cmd: cmd.cmd, channel: msg.channel, message: msg, time: cmd.time, repeatDelay: cmd.repeatDelay, member: msg.member, }); if (!cmdScheduler.registerScheduledCommand(newCmd)) { cb('Time is too close to existing command.'); } else { cb(null); } }; /** * Client has requested to change the command prefix for a guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string|number} gId The id of the guild of which to change the * prefix. * @param {string} prefix The new prefix value to set. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete, or has failed. */ this.changePrefix = function changePrefix(userData, socket, gId, prefix, cb) { if (typeof cb !== 'function') cb = function() {}; if (!checkPerm(userData, gId, null, 'changeprefix')) { if (!checkMyGuild(gId)) return; replyNoPerm(socket, 'changePrefix'); cb('Forbidden'); return; } try { self.bot.changePrefix(gId, prefix); } catch (err) { cb('Internal Error'); return; } cb(null); }; /** * Client has requested to change a single raid setting for a guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string|number} gId The id of the guild of which to change the * setting. * @param {string} key The name of the setting to change. * @param {string|boolean} value The value to set the setting to. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete, or has failed. */ this.changeRaidSetting = function changeRaidSetting( userData, socket, gId, key, value, cb) { if (typeof cb !== 'function') cb = function() {}; if (!checkPerm(userData, gId, null, 'lockdown')) { if (!checkMyGuild(gId)) return; replyNoPerm(socket, 'changeRaidSetting'); cb('Forbidden'); return; } if (!raidBlock) { cb('Internal Server Error'); self.common.error( 'Attempted to change RaidBlock settings while raidBlock.js ' + 'is not loaded!', socket.id); return; } const settings = raidBlock.getSettings(gId); if (typeof settings[key] === 'number') { value *= 1; if (isNaN(value)) { cb('Bad Payload'); return; } } if (typeof settings[key] === typeof value) { if (typeof value === 'string' && value.length > 1000) { value = value.substr(0, 1000); } settings[key] = value; } else { cb('Bad Payload'); return; } settings.updated(); cb(null); guildBroadcast(gId, 'raidSettingsChanged', gId); }; /** * Client has requested to change a single ModLog setting for a guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string|number} gId The id of the guild of which to change the * setting. * @param {string} key The name of the setting to change. * @param {string|boolean} value The value to set the setting to. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete, or has failed. */ this.changeModLogSetting = function changeModLogSetting( userData, socket, gId, key, value, cb) { if (!checkMyGuild(gId)) return; if (typeof cb !== 'function') cb = function() {}; if (!checkPerm(userData, gId, null, 'setlogchannel')) { replyNoPerm(socket, 'changeModLogSetting'); cb('Forbidden'); return; } const modLog = self.bot.getSubmodule('./modLog.js'); if (!modLog) { cb('Internal Server Error'); self.common.error( 'Attempted to change ModLog settings while modLog.js is not loaded!', socket.id); return; } const settings = modLog.getSettings(gId); if (typeof settings[key] === 'number') { value *= 1; if (isNaN(value)) { cb('Bad Payload'); return; } } if (key === 'channel') { const channel = self.client.guilds.resolve(gId).channels.resolve(value); if (!channel) { cb('Bad Payload'); return; } else { settings[key] = value; } } else if (typeof settings[key] === typeof value) { settings[key] = value; } else { cb('Bad Payload'); return; } settings.updated(); cb(null); guildBroadcast(gId, 'modLogSettingsChanged', gId); }; /** * Client has requested to change a single command setting for a guild. * * @public * @type {WebSettings~SocketFunction} * @param {object} userData The current user's session data. * @param {socketIo~Socket} socket The socket connection to reply on. * @param {string|number} gId The id of the guild of which to change the * setting. * @param {string} cmd The name of the command to change the setting for. * @param {string} key The name of the setting to change. * @param {string|boolean} value The value to set the setting to, or the key * if changing an enabled or disabled category. * @param {?string} id The ID of the channel, user, or role to change * the setting for if changing the enabled or disabled category. * @param {?boolean} enabled The setting to set the value of the ID setting. * @param {WebSettings~basicCB} [cb] Callback that fires once the requested * action is complete, or has failed. */ this.changeCommandSetting = function changeCommandSetting( userData, socket, gId, cmd, key, value, id, enabled, cb) { if (!checkMyGuild(gId)) return; if (typeof cb !== 'function') cb = function() {}; if (!checkPerm(userData, gId, null, 'enable') || !checkPerm(userData, gId, null, 'disable')) { replyNoPerm(socket, 'changeCommandSetting'); cb('Forbidden'); return; } const command = self.command.find(cmd); if (!command) { cb('Bad Payload'); return; } const userSettings = self.command.getUserSettings(gId); const name = command.getFullName(); if (!userSettings[name]) { userSettings[name] = new self.command.CommandSetting(command.options); } const setting = userSettings[name]; if (typeof setting[key] === 'object' && typeof value === 'string') { if (typeof id !== 'string' || typeof setting[key][value] === 'undefined') { cb('Bad Payload'); return; } else { if (enabled === true) { setting[key][value][id] = true; } else if (enabled === false) { delete setting[key][value][id]; } else { cb('Bad Payload'); return; } } } else if (typeof setting[key] !== typeof value) { cb('Bad Payload'); return; } else { setting[key] = value; } setting.updated(); cb(null); guildBroadcast(gId, 'commandSettingsChanged', gId); }; } module.exports = new WebSettings();