Source: web/settings.js

// 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();