<commit_before>/**
 * This is the file where the bot commands are located
 *
 * @license MIT license
 */

/// <reference path="typings/node.d.ts" />

import {Config} from './main'
import {Room, getRoom} from './rooms'
import {User, self, getUser} from './users'
import {isEmpty, toId} from './utils'
import {
    chatData, settings, writeSettings, blacklistUser,
    unblacklistUser, uploadToHastebin, getTimeAgo
} from './parser'

import {request} from 'http'

// .set constants
const CONFIGURABLE_COMMANDS: {[name: string]: boolean} = {
    autoban: true,
    banword: true,
    say: true,
    joke: true,
    usagestats: true,
    '8ball': true,
    studio: true,
    buzz: true,
}

const CONFIGURABLE_MODERATION_OPTIONS: {[name: string]: boolean} = {
    flooding: true,
    caps: true,
    stretching: true,
    bannedwords: true,
    groupchat: true,
}

const CONFIGURABLE_COMMAND_LEVELS: {[name: string]: boolean|string} = {
    off: false,
    disable: false,
    'false': false,
    on: true,
    enable: true,
    'true': true
}

for (const i in Config.groups) {
    if (i !== ' ') CONFIGURABLE_COMMAND_LEVELS[i] = i
}

function stripCommands(text: string) {
    text = text.trim()
    if (text.charAt(0) === '/') return '/' + text
    if (text.charAt(0) === '!' || /^>>>? /.test(text)) return ' ' + text
    return text
}

const commands: {
    [name: string]: string
    | ((arg: string, user: User, room: Room|User) => boolean)
    | ((arg: string, user: User, room: Room|User) => void)
} = {
    /**
     * Help commands
     *
     * These commands are here to provide information about the bot.
     */

    credits: 'about',
    about(arg, user, room) {
        const target = user.hasRank(room.id, '#') ? room : user
        const text = '**Pokémon Showdown Bot** by: Quinella, TalkTakesTime, and Morfent'
        target.say(text)
    },
    git(arg, user, room) {
        const target = user.isExcepted() ? room : user
        const text = '**Pokemon Showdown Bot** source code: ' + Config.fork
        target.say(text)
    },
    help: 'guide',
    guide(arg, user, room) {
        const target = user.hasRank(room.id, '#') ? room : user
        let text: string
        if (Config.botguide) {
            text = 'A guide on how to use this bot can be found here: ' + Config.botguide
        } else {
            text = 'There is no guide for this bot. PM the owner with any questions.'
        }
        target.say(text)
    },

    /**
     * Dev commands
     *
     * These commands are here for highly ranked users (or the creator) to use
     * to perform arbitrary actions that can't be done through any other commands
     * or to help with upkeep of the bot.
     */

    custom(arg, user, room) {
        if (!user.isExcepted()) return false
        // Custom commands can be executed in an arbitrary room using the syntax
        // ".custom [room] command", e.g., to do !data pikachu in the room lobby,
        // the command would be ".custom [lobby] !data pikachu". However, using
        // "[" and "]" in the custom command to be executed can mess this up, so
        // be careful with them.
        if (arg.indexOf('[') !== 0 || arg.indexOf(']') < 0) {
            return room.say(arg)
        }
        const tarRoomid = arg.slice(1, arg.indexOf(']'))
        const tarRoom = getRoom(tarRoomid)
        if (!tarRoom) return room.say(self.name + ' is not in room ' + tarRoomid + '!')
        arg = arg.substr(arg.indexOf(']') + 1).trim()
        tarRoom.say(arg)
    },
    js(arg, user, room) {
        if (!user.isExcepted()) return false
        try {
            const result = eval(arg.trim())
            room.say(JSON.stringify(result))
        } catch (e) {
            room.say(e.name + ": " + e.message)
        }
    },
    uptime(arg, user, room) {
        let text = ((room === user || user.isExcepted()) ? '' : '/pm ' + user.id + ', ') + '**Uptime:** '
        const divisors = [52, 7, 24, 60, 60]
        const units = ['week', 'day', 'hour', 'minute', 'second']
        const buffer: string[] = []
        let uptime = ~~(process.uptime())
        do {
            const divisor = divisors.pop()
            const unit = uptime % divisor
            buffer.push(unit > 1 ? unit + ' ' + units.pop() + 's' : unit + ' ' + units.pop())
            uptime = ~~(uptime / divisor)
        } while (uptime)

        switch (buffer.length) {
        case 5:
            text += buffer[4] + ', '
            /* falls through */
        case 4:
            text += buffer[3] + ', '
            /* falls through */
        case 3:
            text += buffer[2] + ', ' + buffer[1] + ', and ' + buffer[0]
            break
        case 2:
            text += buffer[1] + ' and ' + buffer[0]
            break
        case 1:
            text += buffer[0]
            break
        }

        room.say(text)
    },


    /**
     * Room Owner commands
     *
     * These commands allow room owners to personalise settings for moderation and command use.
     */

    settings: 'set',
    set(arg, user, room) {
        if (room === user || !user.hasRank(room.id, '#')) return false

        const opts = arg.split(',')
        let cmd = toId(opts[0])
        const roomid = room.id
        if (cmd === 'm' || cmd === 'mod' || cmd === 'modding') {
            let modOpt: string
            if (!opts[1] || !CONFIGURABLE_MODERATION_OPTIONS[(modOpt = toId(opts[1]))]) {
                return room.say('Incorrect command: correct syntax is ' + Config.commandcharacter + 'set mod, [' +
                    Object.keys(CONFIGURABLE_MODERATION_OPTIONS).join('/') + '](, [on/off])')
            }
            if (!opts[2]) return room.say('Moderation for ' + modOpt + ' in this room is currently ' +
                (settings.modding && settings.modding[roomid] && modOpt in settings.modding[roomid] ? 'OFF' : 'ON') + '.')

            if (!settings.modding) settings.modding = {}
            if (!settings.modding[roomid]) settings.modding[roomid] = {}

            const setting = toId(opts[2])
            if (setting === 'on') {
                delete settings.modding[roomid][modOpt]
                if (isEmpty(settings.modding[roomid])) delete settings.modding[roomid]
                if (isEmpty(settings.modding)) delete settings.modding
            } else if (setting === 'off') {
                settings.modding[roomid][modOpt] = 0
            } else {
                return room.say('Incorrect command: correct syntax is ' + Config.commandcharacter + 'set mod, [' +
                    Object.keys(CONFIGURABLE_MODERATION_OPTIONS).join('/') + '](, [on/off])')
            }

            writeSettings()
            return room.say('Moderation for ' + modOpt + ' in this room is now ' + setting.toUpperCase() + '.')
        }

        if (!(cmd in commands)) return room.say(Config.commandcharacter + '' + opts[0] + ' is not a valid command.')

        let failsafe = 0
        while (true) {
            const newCommand = commands[cmd]
            if (typeof newCommand === 'string') {
                cmd = newCommand
            } else if (typeof newCommand === 'function') {
                if (cmd in CONFIGURABLE_COMMANDS) break
                return room.say('The settings for ' + Config.commandcharacter + '' + opts[0] + ' cannot be changed.')
            } else {
                return room.say('Something went wrong. PM Morfent or TalkTakesTime here or on Smogon with the command you tried.')
            }

            if (++failsafe > 5) return room.say('The command "' + Config.commandcharacter + '' + opts[0] + '" could not be found.')
        }

        if (!opts[1]) {
            let msg = '' + Config.commandcharacter + '' + cmd + ' is '
            if (!settings[cmd] || (!(roomid in settings[cmd]))) {
                msg += 'available for users of rank ' + ((cmd === 'autoban' || cmd === 'banword') ? '#' : Config.defaultrank) + ' and above.'
            } else if (<string> settings[cmd][roomid] in CONFIGURABLE_COMMAND_LEVELS) {
                msg += 'available for users of rank ' + settings[cmd][roomid] + ' and above.'
            } else {
                msg += settings[cmd][roomid] ? 'available for all users in this room.' : 'not available for use in this room.'
            }

            return room.say(msg)
        }

        const setting = opts[1].trim()
        if (!(setting in CONFIGURABLE_COMMAND_LEVELS)) return room.say('Unknown option: "' + setting + '". Valid settings are: off/disable/false, +, %, @, #, &, ~, on/enable/true.')
        if (!settings[cmd]) settings[cmd] = {}
        settings[cmd][roomid] = CONFIGURABLE_COMMAND_LEVELS[setting]

        writeSettings()
        room.say('The command ' + Config.commandcharacter + '' + cmd + ' is now ' +
            (CONFIGURABLE_COMMAND_LEVELS[setting] === setting ? ' available for users of rank ' + setting + ' and above.' :
            (settings[cmd][roomid] ? 'available for all users in this room.' : 'unavailable for use in this room.')))
    },
    blacklist: 'autoban',
    ban: 'autoban',
    ab: 'autoban',
    autoban(arg, user, room) {
        if (room === user || !user.canUse('autoban', room.id)) return false
        if (!self.hasRank(room.id, '@')) return room.say(self.name + ' requires rank of @ or higher to (un)blacklist.')
        if (!toId(arg)) return room.say('You must specify at least one user to blacklist.')

        const args = arg.split(',')
        const added: string[] = []
        const illegalNick: string[] = []
        const alreadyAdded: string[] = []
        const roomid = room.id
        for (let u of args) {
            const tarUser = toId(u)
            if (!tarUser || tarUser.length > 18) {
                illegalNick.push(tarUser)
            } else if (!blacklistUser(tarUser, roomid)) {
                alreadyAdded.push(tarUser)
            } else {
                added.push(tarUser)
                room.say('/roomban ' + tarUser + ', Blacklisted user')
            }
        }

        let text = ''
        if (added.length) {
            text += 'User' + (added.length > 1 ? 's "' + added.join('", "') + '" were' : ' "' + added[0] + '" was') + ' added to the blacklist.'
            room.say('/modnote ' + text + ' by ' + user.name + '.')
            writeSettings()
        }
        if (alreadyAdded.length) {
            text += ' User' + (alreadyAdded.length > 1 ? 's "' + alreadyAdded.join('", "') + '" are' : ' "' + alreadyAdded[0] + '" is') + ' already present in the blacklist.'
        }
        if (illegalNick.length) text += (text ? ' All other' : 'All') + ' users had illegal nicks and were not blacklisted.'
        room.say(text)
    },
    unblacklist: 'unautoban',
    unban: 'unautoban',
    unab: 'unautoban',
    unautoban(arg, user, room) {
        if (room === user || !user.canUse('autoban', room.id)) return false
        if (!self.hasRank(room.id, '@')) return room.say(self.name + ' requires rank of @ or higher to (un)blacklist.')
        if (!toId(arg)) return room.say('You must specify at least one user to unblacklist.')

        const args = arg.split(',')
        const removed: string[] = []
        const notRemoved: string[] = []
        const roomid = room.id
        for (let u of args) {
            const tarUser = toId(u)
            if (!tarUser || tarUser.length > 18) {
                notRemoved.push(tarUser)
            } else if (!unblacklistUser(tarUser, roomid)) {
                notRemoved.push(tarUser)
            } else {
                removed.push(tarUser)
                room.say('/roomunban ' + tarUser)
            }
        }

        let text = ''
        if (removed.length) {
            text += ' User' + (removed.length > 1 ? 's "' + removed.join('", "') + '" were' : ' "' + removed[0] + '" was') + ' removed from the blacklist'
            room.say('/modnote ' + text + ' by user ' + user.name + '.')
            writeSettings()
        }
        if (notRemoved.length) text += (text.length ? ' No other' : 'No') + ' specified users were present in the blacklist.'
        room.say(text)
    },
    rab: 'regexautoban',
    regexautoban(arg, user, room) {
        if (room === user || !user.isRegexWhitelisted() || !user.canUse('autoban', room.id)) return false
        if (!self.hasRank(room.id, '@')) return room.say(self.name + ' requires rank of @ or higher to (un)blacklist.')
        if (!arg) return room.say('You must specify a regular expression to (un)blacklist.')

        try {
            new RegExp(arg, 'i')
        } catch (e) {
            return room.say(e.message)
        }

        if (/^(?:(?:\.+|[a-z0-9]|\\[a-z0-9SbB])(?![a-z0-9\.\\])(?:\*|\{\d+\,(?:\d+)?\}))+$/i.test(arg)) {
            return room.say('Regular expression /' + arg + '/i cannot be added to the blacklist. Don\'t be Machiavellian!')
        }

        const regex = '/' + arg + '/i'
        if (!blacklistUser(regex, room.id)) return room.say('/' + regex + ' is already present in the blacklist.')

        const regexObj = new RegExp(arg, 'i')
        // Deal with TypeScript limitations by completely ignoring strict typing.
        const users = (<any> Array).from((<any> room).users.entries())
        const groups = Config.groups
        const selfid = self.id<commit_msg>users variable in rab is string[].<commit_after>/**
 * This is the file where the bot commands are located
 *
 * @license MIT license
 */

/// <reference path="typings/node.d.ts" />

import {Config} from './main'
import {Room, getRoom} from './rooms'
import {User, self, getUser} from './users'
import {isEmpty, toId} from './utils'
import {
    chatData, settings, writeSettings, blacklistUser,
    unblacklistUser, uploadToHastebin, getTimeAgo
} from './parser'

import {request} from 'http'

// .set constants
const CONFIGURABLE_COMMANDS: {[name: string]: boolean} = {
    autoban: true,
    banword: true,
    say: true,
    joke: true,
    usagestats: true,
    '8ball': true,
    studio: true,
    buzz: true,
}

const CONFIGURABLE_MODERATION_OPTIONS: {[name: string]: boolean} = {
    flooding: true,
    caps: true,
    stretching: true,
    bannedwords: true,
    groupchat: true,
}

const CONFIGURABLE_COMMAND_LEVELS: {[name: string]: boolean|string} = {
    off: false,
    disable: false,
    'false': false,
    on: true,
    enable: true,
    'true': true
}

for (const i in Config.groups) {
    if (i !== ' ') CONFIGURABLE_COMMAND_LEVELS[i] = i
}

function stripCommands(text: string) {
    text = text.trim()
    if (text.charAt(0) === '/') return '/' + text
    if (text.charAt(0) === '!' || /^>>>? /.test(text)) return ' ' + text
    return text
}

const commands: {
    [name: string]: string
    | ((arg: string, user: User, room: Room|User) => boolean)
    | ((arg: string, user: User, room: Room|User) => void)
} = {
    /**
     * Help commands
     *
     * These commands are here to provide information about the bot.
     */

    credits: 'about',
    about(arg, user, room) {
        const target = user.hasRank(room.id, '#') ? room : user
        const text = '**Pokémon Showdown Bot** by: Quinella, TalkTakesTime, and Morfent'
        target.say(text)
    },
    git(arg, user, room) {
        const target = user.isExcepted() ? room : user
        const text = '**Pokemon Showdown Bot** source code: ' + Config.fork
        target.say(text)
    },
    help: 'guide',
    guide(arg, user, room) {
        const target = user.hasRank(room.id, '#') ? room : user
        let text: string
        if (Config.botguide) {
            text = 'A guide on how to use this bot can be found here: ' + Config.botguide
        } else {
            text = 'There is no guide for this bot. PM the owner with any questions.'
        }
        target.say(text)
    },

    /**
     * Dev commands
     *
     * These commands are here for highly ranked users (or the creator) to use
     * to perform arbitrary actions that can't be done through any other commands
     * or to help with upkeep of the bot.
     */

    custom(arg, user, room) {
        if (!user.isExcepted()) return false
        // Custom commands can be executed in an arbitrary room using the syntax
        // ".custom [room] command", e.g., to do !data pikachu in the room lobby,
        // the command would be ".custom [lobby] !data pikachu". However, using
        // "[" and "]" in the custom command to be executed can mess this up, so
        // be careful with them.
        if (arg.indexOf('[') !== 0 || arg.indexOf(']') < 0) {
            return room.say(arg)
        }
        const tarRoomid = arg.slice(1, arg.indexOf(']'))
        const tarRoom = getRoom(tarRoomid)
        if (!tarRoom) return room.say(self.name + ' is not in room ' + tarRoomid + '!')
        arg = arg.substr(arg.indexOf(']') + 1).trim()
        tarRoom.say(arg)
    },
    js(arg, user, room) {
        if (!user.isExcepted()) return false
        try {
            const result = eval(arg.trim())
            room.say(JSON.stringify(result))
        } catch (e) {
            room.say(e.name + ": " + e.message)
        }
    },
    uptime(arg, user, room) {
        let text = ((room === user || user.isExcepted()) ? '' : '/pm ' + user.id + ', ') + '**Uptime:** '
        const divisors = [52, 7, 24, 60, 60]
        const units = ['week', 'day', 'hour', 'minute', 'second']
        const buffer: string[] = []
        let uptime = ~~(process.uptime())
        do {
            const divisor = divisors.pop()
            const unit = uptime % divisor
            buffer.push(unit > 1 ? unit + ' ' + units.pop() + 's' : unit + ' ' + units.pop())
            uptime = ~~(uptime / divisor)
        } while (uptime)

        switch (buffer.length) {
        case 5:
            text += buffer[4] + ', '
            /* falls through */
        case 4:
            text += buffer[3] + ', '
            /* falls through */
        case 3:
            text += buffer[2] + ', ' + buffer[1] + ', and ' + buffer[0]
            break
        case 2:
            text += buffer[1] + ' and ' + buffer[0]
            break
        case 1:
            text += buffer[0]
            break
        }

        room.say(text)
    },


    /**
     * Room Owner commands
     *
     * These commands allow room owners to personalise settings for moderation and command use.
     */

    settings: 'set',
    set(arg, user, room) {
        if (room === user || !user.hasRank(room.id, '#')) return false

        const opts = arg.split(',')
        let cmd = toId(opts[0])
        const roomid = room.id
        if (cmd === 'm' || cmd === 'mod' || cmd === 'modding') {
            let modOpt: string
            if (!opts[1] || !CONFIGURABLE_MODERATION_OPTIONS[(modOpt = toId(opts[1]))]) {
                return room.say('Incorrect command: correct syntax is ' + Config.commandcharacter + 'set mod, [' +
                    Object.keys(CONFIGURABLE_MODERATION_OPTIONS).join('/') + '](, [on/off])')
            }
            if (!opts[2]) return room.say('Moderation for ' + modOpt + ' in this room is currently ' +
                (settings.modding && settings.modding[roomid] && modOpt in settings.modding[roomid] ? 'OFF' : 'ON') + '.')

            if (!settings.modding) settings.modding = {}
            if (!settings.modding[roomid]) settings.modding[roomid] = {}

            const setting = toId(opts[2])
            if (setting === 'on') {
                delete settings.modding[roomid][modOpt]
                if (isEmpty(settings.modding[roomid])) delete settings.modding[roomid]
                if (isEmpty(settings.modding)) delete settings.modding
            } else if (setting === 'off') {
                settings.modding[roomid][modOpt] = 0
            } else {
                return room.say('Incorrect command: correct syntax is ' + Config.commandcharacter + 'set mod, [' +
                    Object.keys(CONFIGURABLE_MODERATION_OPTIONS).join('/') + '](, [on/off])')
            }

            writeSettings()
            return room.say('Moderation for ' + modOpt + ' in this room is now ' + setting.toUpperCase() + '.')
        }

        if (!(cmd in commands)) return room.say(Config.commandcharacter + '' + opts[0] + ' is not a valid command.')

        let failsafe = 0
        while (true) {
            const newCommand = commands[cmd]
            if (typeof newCommand === 'string') {
                cmd = newCommand
            } else if (typeof newCommand === 'function') {
                if (cmd in CONFIGURABLE_COMMANDS) break
                return room.say('The settings for ' + Config.commandcharacter + '' + opts[0] + ' cannot be changed.')
            } else {
                return room.say('Something went wrong. PM Morfent or TalkTakesTime here or on Smogon with the command you tried.')
            }

            if (++failsafe > 5) return room.say('The command "' + Config.commandcharacter + '' + opts[0] + '" could not be found.')
        }

        if (!opts[1]) {
            let msg = '' + Config.commandcharacter + '' + cmd + ' is '
            if (!settings[cmd] || (!(roomid in settings[cmd]))) {
                msg += 'available for users of rank ' + ((cmd === 'autoban' || cmd === 'banword') ? '#' : Config.defaultrank) + ' and above.'
            } else if (<string> settings[cmd][roomid] in CONFIGURABLE_COMMAND_LEVELS) {
                msg += 'available for users of rank ' + settings[cmd][roomid] + ' and above.'
            } else {
                msg += settings[cmd][roomid] ? 'available for all users in this room.' : 'not available for use in this room.'
            }

            return room.say(msg)
        }

        const setting = opts[1].trim()
        if (!(setting in CONFIGURABLE_COMMAND_LEVELS)) return room.say('Unknown option: "' + setting + '". Valid settings are: off/disable/false, +, %, @, #, &, ~, on/enable/true.')
        if (!settings[cmd]) settings[cmd] = {}
        settings[cmd][roomid] = CONFIGURABLE_COMMAND_LEVELS[setting]

        writeSettings()
        room.say('The command ' + Config.commandcharacter + '' + cmd + ' is now ' +
            (CONFIGURABLE_COMMAND_LEVELS[setting] === setting ? ' available for users of rank ' + setting + ' and above.' :
            (settings[cmd][roomid] ? 'available for all users in this room.' : 'unavailable for use in this room.')))
    },
    blacklist: 'autoban',
    ban: 'autoban',
    ab: 'autoban',
    autoban(arg, user, room) {
        if (room === user || !user.canUse('autoban', room.id)) return false
        if (!self.hasRank(room.id, '@')) return room.say(self.name + ' requires rank of @ or higher to (un)blacklist.')
        if (!toId(arg)) return room.say('You must specify at least one user to blacklist.')

        const args = arg.split(',')
        const added: string[] = []
        const illegalNick: string[] = []
        const alreadyAdded: string[] = []
        const roomid = room.id
        for (let u of args) {
            const tarUser = toId(u)
            if (!tarUser || tarUser.length > 18) {
                illegalNick.push(tarUser)
            } else if (!blacklistUser(tarUser, roomid)) {
                alreadyAdded.push(tarUser)
            } else {
                added.push(tarUser)
                room.say('/roomban ' + tarUser + ', Blacklisted user')
            }
        }

        let text = ''
        if (added.length) {
            text += 'User' + (added.length > 1 ? 's "' + added.join('", "') + '" were' : ' "' + added[0] + '" was') + ' added to the blacklist.'
            room.say('/modnote ' + text + ' by ' + user.name + '.')
            writeSettings()
        }
        if (alreadyAdded.length) {
            text += ' User' + (alreadyAdded.length > 1 ? 's "' + alreadyAdded.join('", "') + '" are' : ' "' + alreadyAdded[0] + '" is') + ' already present in the blacklist.'
        }
        if (illegalNick.length) text += (text ? ' All other' : 'All') + ' users had illegal nicks and were not blacklisted.'
        room.say(text)
    },
    unblacklist: 'unautoban',
    unban: 'unautoban',
    unab: 'unautoban',
    unautoban(arg, user, room) {
        if (room === user || !user.canUse('autoban', room.id)) return false
        if (!self.hasRank(room.id, '@')) return room.say(self.name + ' requires rank of @ or higher to (un)blacklist.')
        if (!toId(arg)) return room.say('You must specify at least one user to unblacklist.')

        const args = arg.split(',')
        const removed: string[] = []
        const notRemoved: string[] = []
        const roomid = room.id
        for (let u of args) {
            const tarUser = toId(u)
            if (!tarUser || tarUser.length > 18) {
                notRemoved.push(tarUser)
            } else if (!unblacklistUser(tarUser, roomid)) {
                notRemoved.push(tarUser)
            } else {
                removed.push(tarUser)
                room.say('/roomunban ' + tarUser)
            }
        }

        let text = ''
        if (removed.length) {
            text += ' User' + (removed.length > 1 ? 's "' + removed.join('", "') + '" were' : ' "' + removed[0] + '" was') + ' removed from the blacklist'
            room.say('/modnote ' + text + ' by user ' + user.name + '.')
            writeSettings()
        }
        if (notRemoved.length) text += (text.length ? ' No other' : 'No') + ' specified users were present in the blacklist.'
        room.say(text)
    },
    rab: 'regexautoban',
    regexautoban(arg, user, room) {
        if (room === user || !user.isRegexWhitelisted() || !user.canUse('autoban', room.id)) return false
        if (!self.hasRank(room.id, '@')) return room.say(self.name + ' requires rank of @ or higher to (un)blacklist.')
        if (!arg) return room.say('You must specify a regular expression to (un)blacklist.')

        try {
            new RegExp(arg, 'i')
        } catch (e) {
            return room.say(e.message)
        }

        if (/^(?:(?:\.+|[a-z0-9]|\\[a-z0-9SbB])(?![a-z0-9\.\\])(?:\*|\{\d+\,(?:\d+)?\}))+$/i.test(arg)) {
            return room.say('Regular expression /' + arg + '/i cannot be added to the blacklist. Don\'t be Machiavellian!')
        }

        const regex = '/' + arg + '/i'
        if (!blacklistUser(regex, room.id)) return room.say('/' + regex + ' is already present in the blacklist.')

        const regexObj = new RegExp(arg, 'i')
        // Deal with TypeScript limitations by completely ignoring strict typing.
        const users: string[] = (<any> Array).from((<any> room).users.entries())
        const groups = Config.groups
        const selfid = self.id
{'fraction_non_alphanumeric': 0.15341101934607987, 'fraction_numerical': 0.004449975487423163, 'mean_word_length': 2.035485347985348, 'pattern_counts': {'":': 2, '<': 13, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2581106', 'n_tokens_mistral': 8175, 'n_tokens_neox': 7988, 'n_words': 2259}