src/blacklist.js

const FulfillmentError = require('./fulfillment_error.js');

const BLACKLIST_PERSISTENCE_KEY = 'blacklist';

function getWriteableChannel(bot, guild) {
  return guild.channels.find(
    channel => channel.type === 0 && channel.permissionsOf(bot.user.id).json.sendMessages);
}

async function leaveGuildWithExplanation(bot, guild, reason) {
  const writeableChannel = getWriteableChannel(bot, guild);
  if (writeableChannel) {
    await writeableChannel.createMessage(`I'm leaving this guild because its owner was blacklisted from using me.\n\nBlacklist reason: \`\`\`${reason}\`\`\``);
  }

  return guild.leave();
}

function leaveGuildsWithExplanation(bot, guilds, reason) {
  return Promise.all(guilds.map(guild => leaveGuildWithExplanation(bot, guild, reason)));
}

/**
 * Maintains a list of blacklisted users with whom the bot should not interact.
 * The final resting place of command spammers and people you don't like.
 * The Blacklist can be accessed via {@link Monochrome#getBlacklist}.
 * See [the demo blacklist]{@link https://github.com/mistval/monochrome-demo/blob/master/commands/blacklist.js}
 * and [demo unblacklist]{@link https://github.com/mistval/monochrome-demo/blob/master/commands/unblacklist.js}
 * commands for examples of using the blacklist. The demo commands can be used in your bot without requiring any modification.
 * @hideconstructor
 */
class Blacklist {
  constructor(bot, persistence, botAdminIds) {
    this.reasonForUserId_ = {};
    this.persistence_ = persistence;
    this.botAdminIds_ = botAdminIds;
    this.bot_ = bot;

    this.ready = persistence.getData(BLACKLIST_PERSISTENCE_KEY).then(data => {
      this.reasonForUserId_ = data;
    });
  }

  /**
  * Blacklist a user. The user will be completely ignored by the bot, and any guilds
  * that they own will be left.
  * @param {string} userId - The ID of the user to blacklist.
  * @param {string} reason - The reason the user was blacklisted. If the user is a guild owner,
  *   the bot will send a message with this reason to a channel in the guild before leaving.
  *   If the user is not a guild owner, they will just be silently ignored and will not see
  *   this reason.
  */
  async blacklistUser(userId, reason) {
    await this.ready;
    if (this.botAdminIds_.indexOf(userId) !== -1) {
      throw new FulfillmentError({
        publicMessage: `<@${userId}> is a bot admin and can't be blacklisted.`,
        logDescription: 'User is a bot admin',
      });
    }

    this.reasonForUserId_[userId] = reason;
    await this.updatePersistence_();

    const blacklistedGuilds = this.bot_.guilds.filter(guild => guild.ownerID === userId);
    return leaveGuildsWithExplanation(this.bot_, blacklistedGuilds, reason);
  }

  async leaveGuildIfBlacklisted(guild) {
    await this.ready;
    const blacklisted = await this.isUserBlacklisted(guild.ownerID);
    if (!blacklisted) {
      return false;
    }

    const reason = this.reasonForUserId_[guild.ownerID];
    await leaveGuildWithExplanation(this.bot_, guild, reason);
    return true;
  }

  /**
   * Remove a user from the blacklist so that they can interact with the bot again.
   * @param {string} userId - The ID of the user to unblacklist.
   */
  async unblacklistUser(userId) {
    await this.ready;
    delete this.reasonForUserId_[userId];
    return this.updatePersistence_();
  }

  /**
   * Check if a user is blacklisted without first checking if the blacklist
   * has loaded. This function is meant to be called in hot paths and may incorrectly
   * return false if called immediately after the bot is started. Consider using
   * {@link Blacklist#isUserBlacklisted} instead. It will always return the correct result.
   * @param {string} userId - The ID of the user to check the blacklist for.
   * @return {boolean}
   */
  isUserBlacklistedQuick(userId) {
    return !!this.reasonForUserId_[userId];
  }

  /**
   * Check if a user is blacklisted.
   * @param {string} userId - The ID of the user to check the blacklist for.
   * @return {boolean}
   */
  async isUserBlacklisted(userId) {
    await this.ready;
    return !!this.reasonForUserId_[userId];
  }

  async updatePersistence_() {
    await this.ready;
    return this.persistence_.editData(BLACKLIST_PERSISTENCE_KEY, () => this.reasonForUserId_);
  }
}

module.exports = Blacklist;