6
\$\begingroup\$

I've been inspired by @SirPython's SirAlfred JavaScript chat bot, so I went and made my own. This one is slightly different though, in the fact that it can accept input in a more lenient way. For example, here's a "conversation" with the bot:

sudo make me a sandwich bot
BOT: It will be done master.
sudo make me a sandwich bot
BOT: Yes sir.
Hello there bot.
BOT: Hello there.
Goodbye bot.
BOT: Goodbye.

Essentially, instead of directly checking the contents of the string, it removes all non-alphanumeric characters, excluding spaces, and then splits on a space. It then checks to see if this split array contains certain words, and then outputs a message based on those words.

/**
 * This is a small utility function used to
 * determine if an array contains a certain
 * value.
 * @param {array} array - The array to check for the element in.
 * @param {generic} value   - The value to check for.
 * @returns {boolean}
 */
function arrayContains(array, value) {
    return array.indexOf(value) > -1;
}

/**
 * Select a random element from the array.
 * @param {array} array - The array to select from.
 */
function randomArrayElement(array) {
    return array[Math.floor(Math.random() * array.length)];
}

/**
 * This error is thrown when sendMessage
 * is called and the message length
 * exceeds 400 characters.
 * @constructor
 * @param {string} message - The error's message.
 */
function InvalidMessageLength(message) {
    this.message = message;
    this.name = "InvalidMessageLength";
}

/**
 * Post a message to the room.
 * @param {string} messageString - The string to be posted.
 */
function sendMessage(messageString) {
    if(messageString.length - 1 <= 400) {
        document.getElementById("input").value = messageString;
        document.getElementById("sayit-button").click();
    }
    else {
        throw new InvalidMessageLength("Length of message exceeded 400 characters.");
    }
}

/**
 * Obtains the very last posted message.
 * @returns {object}
 */
function getLastPostedMessage() {
    try {
        var messageElements = document.getElementById("chat");
        return messageElements.lastElementChild.children[1].lastElementChild.children[1].innerHTML;
    }
    catch(error) {
        // The reason I have this try-catch block set up
        // is because for some reason, an error associated
        // with innerHTML pops up, and I don't know why.
        // Sometimes it works fine, other times, the error
        // will pop up after n amount of messages. Maybe
        // it's a browser compatability issue?
        return "";
    }
}

/**
 * Gets user input and sends messages
 * back to that user.
 */
function main() {
    var BOT_TEXT = "**`BOT:`** ";
    var RANDOM_REPLIES = {
        helloGreeting: function() {
            return randomArrayElement([
                "Hello.",
                "Hello there.",
                "Hello user."
            ]);
        },
        goodbyeGreeting: function() {
            return randomArrayElement([
                "Goodbye.",
                "Goodbye user."
            ]);
        },
        sudoCommand: function() {
            return randomArrayElement([
                "Yes master.",
                "Yes sir.",
                "It will be done master."
            ]);
        }
    }

    var lastMessage = getLastPostedMessage().toLowerCase().replace(/[^a-zA-Z\d\s:]/g, "").split(" ");
    if(arrayContains(lastMessage, "hello") && arrayContains(lastMessage, "bot")) {
        sendMessage(BOT_TEXT + RANDOM_REPLIES.helloGreeting());
    }
    else if(arrayContains(lastMessage, "goodbye") && arrayContains(lastMessage, "bot")) {
        sendMessage(BOT_TEXT + RANDOM_REPLIES.goodbyeGreeting());
    }
    else if(arrayContains(lastMessage, "sudo") && arrayContains(lastMessage, "bot")) {
        sendMessage(BOT_TEXT + RANDOM_REPLIES.sudoCommand());
    }
    window.setTimeout(main, 4500);
}

main();

Finally, in order to run this, you need to open up inspect element on your browser. While in a Stack Exchange chat room, copy-paste the above source into the JavaScript console, and then hit enter.

\$\endgroup\$
6
\$\begingroup\$
if(arrayContains(lastMessage, "hello") && arrayContains(lastMessage, "bot")) {
    sendMessage(BOT_TEXT + RANDOM_REPLIES.helloGreeting());
}
else if(arrayContains(lastMessage, "goodbye") && arrayContains(lastMessage, "bot")) {
    sendMessage(BOT_TEXT + RANDOM_REPLIES.goodbyeGreeting());
}
else if(arrayContains(lastMessage, "sudo") && arrayContains(lastMessage, "bot")) {
    sendMessage(BOT_TEXT + RANDOM_REPLIES.sudoCommand());
}

What's the difference between these three lines? The keyword being searched for and the method being called.

Just two values? Seems like the perfect job for a map/object!

I'm assuming that you aren't going to only have these three commands for your chat bot; you're probably going to want to add more in the future.

Let's use a map/object (as I stated above) to store these commands and their keywords and functions:

var commands = {
    "hello": function() { [hello greeting] },
    "goodbye": function() { [goodbye greeting] },
    "sudo": function() { [sudo command] }
};

Now, going back to the main function, all we have to do now is iterate through the map/object and check if the key value with the keyword that you were originally passing into arrayContains.

Here is what that looks like:

var lastMessage = getLastPostedMessage().toLowerCase().replace(/[^a-zA-Z\d\s:]/g, "").split(" ");

if(arrayContains(lastMessage, "bot")) {
    for(var command in commands) {
        if(arrayContains(lastMessage, command)) {
            sendMessage(BOT_TEXT + commands[command]());
        }
    }
}

Note: I moved the arrayContains outside of the loop because you don't want to unnecessarily check if the message is a command if it isn't even addressing the bot. Credit to 3Doubloons.


I just realized this, but your RANDOM_REPLIES object could be used instead of creating that new commands map/object that I recommended. The only thing you'd have to change in that object would be the keys; you'd have to change those to the keywords.

\$\endgroup\$
  • \$\begingroup\$ You could probably hoist the arrayContains(..., 'bot') check outside the loop. \$\endgroup\$ – 3Doubloons Jul 5 '15 at 3:19
  • \$\begingroup\$ @3Doubloons I modified my answer to accommodate your recommendation. \$\endgroup\$ – SirPython Jul 5 '15 at 14:37
3
\$\begingroup\$

JSDoc

I must say that your documentation is thorough, easy to read, overall very good! If only all of JavaScript was documented like that it would make the internet a better place!


A few small things not mentioned by SirPython:

"use strict";

Seriously, just do it ! You lose nothing by using strict mode, and you gain the certainty that your variables will always be in the scope you want them to be!

In this case, you could declare it right at the top of your file.


Magic numbers

Right here...

/**
 * This error is thrown when sendMessage
 * is called and the message length
 * exceeds 400 characters.
 * @constructor
 * @param {string} message - The error's message.
 */

Would be a great place to declare a var MAX_MESSAGE_LENGTH = 400; right after.

Then, you can use that throughout:

function sendMessage(messageString) {
    if(messageString.length - 1 <= MAX_MESSAGE_LENGTH) {
        document.getElementById("input").value = messageString;
        document.getElementById("sayit-button").click();
    }
    else {
        throw new InvalidMessageLength("Length of message exceeded " + MAX_MESSAGE_LENGTH + " characters.");
    }
}
\$\endgroup\$
  • \$\begingroup\$ const is a new thing in ES6. I do not believe the OP's code is ES6, so you should use var. \$\endgroup\$ – SirPython Jul 5 '15 at 15:57
  • \$\begingroup\$ Good catch, I made the change :) \$\endgroup\$ – Phrancis Jul 5 '15 at 17:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.