The Bot API - Zirak/SO-ChatBot GitHub Wiki

The bot variable is a variable encapsulating most methods of the - you guessed it - bot. It has a lot of useful shit.

Since the bot has quite a bit of logic, bot functions as both a logic unit (containing properties and methods) and as a namespace for other units (memory, for instance). Let's look at those.

Note: The sections below have an implicit bot prefix. For instance, commands is in reality bot.commands.

Another note: Some methods are more related to the bot's logic, rather than API. To word it differently, some methods are "private", and some are "public". The "private" ones, which you shouldn't really use yourself (but if you want to, go ahead) are said to be internal. They won't be heavily described.

List of Contents

Why are these called tables of contents? It's a list, damnit!

Misc properties

invocationPattern

This is the prefix for bot invocation:

!!listcommands
^^

By default, it's !!, but can be changed to pretty much anything else (including the empty string. Don't do that though).

info

Miscellanous info, used in the /info command.

  • invoked : How many times the bot was called upon (command, listener, eval, ...)
  • learned : How many commands were learned.
  • forgotten : How many commands were forgotten.
  • start : Date of when the bot started.

stoplog

Whether stuff should be logged (the equivalent of a verbose flag in other places). See the next section.

log

bot.log(...args)

If stoplog === true, log args.

stopped

Is the bot stopped (as in, killed)?

stop

bot.stop()

Sets stopped to true.

continue

bot.continue()

Sets stopped to false.

devMode

Whether we're in developer mode. See next section.

activateDevMode

bot.activateDevMode( pattern='beer!' )

Activating dev mode changes some bot interaction:

  • First and most obviously, devMode to true.
  • Changes invocationPattern to pattern (it's beer! by default because of...uh...historical reasons).
  • Removes all IO.events.userjoin listeners (so we won't have any duplicate user join/leave things).
  • Changes validateMessage to only check for the invocationPattern.

Message handling

This section is of all the things responsible to handling a message: From the time it reaches the bot's inner circle, to the time it leaves.

Just as a general overview, here's how an input is treated, from start to end. The outlying stages are heavily related to the IO object and to bot.adapter:

  • An event is received on the websocket, bot.adapter.in.soket, adapter.in.ondata is called. The data (still a string) will look something like this: '{"r1":{"e":[{"event_type":1,"time_stamp":1387026611,"content":"!!help","id":26936905,"user_id":617762,"user_name":"Zirak","room_id":1,"room_name":"Sandbox","message_id":13524880}],"t":26936905,"d":1}}'

  • That string is passed to adapter.in.pollComplete, parsed as JSON, and iterated over. Each key (r1, r17, etc) specifies a roomid, and on each sub-object, the e property holds an array of all the events which happened at the room. For every such event, adapter.in.handleMessageObject is called, passing along the event.

  • handleMessageObject does the base event handling: If it's a user-related event (user joined, user left), it calls handleUserEvent, which just raises the appropriate event. It also possibly handles multiline messages, and finally calls IO.in.receive, pushing the event (the message object) into the queue.

  • After all message objects are dealt with and passed into the input queue, we flush it all down. Now the fun is about to start!

  • bot.parseMessage, which listens to the IO.events.input event, is called, and passed the message object. It validates it against validateMessage to see if we should be bothered by it (does it begin with the invocation pattern? is it sent by someone who isn't us?). We then trim the invocation pattern out, decode html entities, etc etc (see prepareMessage and Message below for details), followed by a ban check (should we respond to that user?). Finally, we act on the message contents: If it's an eval, request? bot.evalit, otherwise try and execute a command/listener (seebot.invokeAction`).

  • Hurray, we did something! Time to send it out! We dress the reply nice and snug, and call bot.adapter.out.add with the output and the roomid.

  • At the final stage of our journey, adapter.out.send calls adapter.out.sendToRoom, which sends an XHR to the chat. All is well in the world.

Okay, time for the methods and shit.

Message

bot.Message( text, msgObj ) -> Message

Wraps text, a string related to msgObj, with some convenience methods. The important thing to note is that msgObj is to know things like which room the text was in, what user sent it, all the stuff we see in a message object (yeah, I intend to write up on raw message objects as well, TODO).

And the famed convenience methods are:

  • send( resp ) - Sends resp to the room.
  • reply( resp ) - Sends resp as a reply to the user (remember, msgObj is our source).
  • directreply( resp ) - Sends resp as a reply to the message.
  • parse() - Parses text as command arguments. It has some other overloads...it's a TODO.
  • exec( regex ) - Used internally in listeners to set some more internal stuff. Not very interesting.
  • findUserid( userName ) - Maps userName to a user id.
  • findUsername( id ) - Maps a userid to a username.
  • codify
  • escape
  • link
  • get( key ) - Gets a property from msgObj, useful for getting the userid and whatnot.
  • set( key, val ) - Sets a property on msgObj.

parseMessage

parseMessage( msgObj )

(Internal) A noble method indeed, the first function our bot sees. Its job is to filter out irrelevant message, and cascade down the relevant ones (once they've been properly pampered).

invokeAction

invokeAction( msg )

(Internal) Given a message, find out if it's a listener or a command, and executes whichever one it is. In case nothing matches, send the user an error.

prepareMessage

prepareMessage( msgObj ) -> bot.Message

(Internalish) Takes the msgObj and does some spring cleaning: Passes it through bot.adapter.transform (which, at the time of writing, actually does nothing), decodes html entities, trims out some insanely weird unicode chars (see #87 and #90), trims the invocation pattern, and finally passes it through bot.Message.

validateMessage

validateMessage( msgObj ) -> true/false

Returns whether the message begins with the invocation pattern, and that the message sender is not the bot itself.

There is an edge-case, #139: If the invocation pattern is !!, we check if the entire message is made out of exclamation marks.

Command-related

commands

The object containing all of the bot's commands, laid out exactly as you suspect: commandName => commandObject. For instance, bot.commands.google looks something like this:

{
    //first, the command properties:
    
    name : 'google',
    description : 'Search Google. `/google query`',
    //the actual command logic. DO NOT CALL THIS DIRECTLY. see exec below.
    fun : function google ( args, cb ) { ... },
    //what `this` will be set to inside `fun`.
    thisArg : { ... },
    //command creator's name. for builtin commands, that's God, for user-taught
    // commands, it's the user's name.
    creator : 'God',
    
    //use/delete permissions
    permissions : {
        del : 'NONE', //default NONE
        use : 'ALL'   //default ALL
    },
    //whether this command CANNOT be used in /tell.
    unTellable : false,

    //async tells you whether the command accepts a callback.
    async : true,
    //how many time the command was invoked (reset on refresh)
    invoked : 4,

    //now for the interesting methods

    //executes the command. pass in the message object and, if the command is
    // async, a callback if you wish. use this to if you want to execute.
    exec : function ( ... ) { ... },

    //deletes the command from the bot.
    del : function () { ... },

    //can the specified usrid use (execute) this command?
    canUse : function ( usrid ) { ... },
    //can the specified usrid delete this command?
    canDel : function ( usrid ) { ... }
}

If you, for some reason, want to juggle commands around in your code, here's the proper etiquette (what's done in the bot):

//to execute
if ( cmdObj.canUse(msgObj.get('user_id')) ) {
    cmdObj.exec( msgObj /*, cb?*/ );
}

//to delete
if ( cmdObj.canDel(msgObj.get('user_id')) ) {
    cmdObj.del();
}

Command

bot.Command( bareCommand ) -> Command

A command factory/constructor/whatever-you-wanna-call-it. You probably won't have to call it directly, since addCommand does this itself. This function fills missing (but necessary) properties on argument has (like privileges and description) and then some (exec, canUse, whatever you saw above).

addCommand

addCommand( cmd )

Adds a command to the bot. You don't have to construct a bot.Command yourself - this does it for you. At the very least, cmd must have name and fun:

bot.addCommand({
    name : 'foo',
    fun : function () { return 'foo!' }
});

At its fullest (without replacing bot.Command):

bot.addCommand({
    name : 'foo',
    fun : function () { return 'foo!' },
    thisArg : { moose : 4 },
    permissions : {
        del : 'ALL',
        use : 'ALL'
    },
    description : 'Just tells you foo.',
    async : false,
    unTellable : false
});

commandExists

commandExists( cmdName ) -> true/false

I hope I don't need to document this method.

getCommand

getCommand( cmdName ) -> bot.Command

If the command exists, return it. Otherwise, return an object containing the error message, and and a list of command suggestions.

execCommand

execCommand( cmd, msg )

(Internal, but no real danger) Cleans up the message (removes command name, leading whitespace and slash, ...) and executes cmd, passing the cleaned message (remember: commands see the message without the invocation parts).

This method is called by invokeAction.

commandDictionary

Used for the "did you mean..." suggestions:

//to look something up
bot.commandDictionary.search( cmdName );

//to change the search max cost (error margin)
bot.commandDictionary.maxCost = 5;
//usually done dynamically according to the search string. what the bot does is:
bot.commandDictionary.maxCost = Math.floor( cmdName.length / 5 + 1 );
//look inside bot.getCommand for it.

//adding something to the dictionary
bot.commandDictionary.trie.add( cmdName );
//to delete something
bot.commandDictionary.trie.del( cmdName );

Listener-related

As a reminder, Listeners are regex based commands. When the bot doesn't recognise an invocation to be a command, it tries executing listeners against the message, and calls the first one which matches.

listeners

An array of listeners. Each listener looks like:

{
    //pattern to test against a message.
    pattern : /^give (.+?) a lick/,
    //what's called when a message matches the pattern.
    fun : function ( msg ) { ... },
    //when `fun` is executed, what its `this` value should be.
    thisArg : null
}

listen

listen( regex, fun, [thisArg] )

Adds a listener, so that when regex matches a message, fun is called.

Note: fun can return false, to indicate that it didn't handle the message after all.

bot.listen(/^woof$/i, function ( msg ) {
    return 'Did you just bark at me?';
});

bot.listen(/^meow$/i, function ( msg ) {
    return msg.get('user_name') + ', you\'re weird.';
});

callListeners

callListeners( msg ) -> true/false

(Internalish) Tries calling the listeners on the passed message. Returns whether any were actually called.

User handling

User handling is...weird. Our source of user-related statistics is the chat itself, which provides some basic information, only related to the room we're in. That's not enough. When we move to the server and have a more-than-decent span of life, it should be improved - especially in regards to cross-room handling (currently, only room owners in the home room are considered actual room owners).

users

Users! A usrid => userObject object, along with some utility methods. bot.users[617762] may look like:

{
	//usrid
	id : 617762,
	name : "Zirak",
	//is the user a room owner? room specific, obviously.
	is_owner : true,
	//is the user a moderator? Site-specific, cross-room.
	is_moderator : false,
	
	//email hash, used for gravatar
	email_hash : "348d335cd13298e475eeaefa1841dee6"
	reputation : 42,
	//some number we don't really care about. doesn't seem to be a msgid.
	last_post : 1387024536
};

There's users.request(roomid, ids, cb), which fetches user information in a specific room (and adds that info to bot.users):

bot.users.request(17, 1839506, function (usr) {
    console.log(usr);
});

//logs
{
	id : 1839506,
	name : 'Caprica Six',
	is_owner : true,
	is_moderator : false,
	
	email_hash : '!http://i.stack.imgur.com/Sej2t.jpg?s=128&g=1',
	reputation : 586
}

If the first argument to request is null, it uses the home roomid.

One thing to note about email_hash since StackExchange introduced the ability to upload your own image instead of gravatar: if the hash begins with a !, it's a link to the image. Otherwise, it's the actual email hash passed to gravatar. You can see the two variations in the examples above.

isOwner

bot.isOwner( usrid ) -> true/false

Returns whether the usrid is a room owner/moderator.

Banlist

Again a simple mapping of usrid, to an object with a boolean told property, used for checking whether we should notify a user he's banned. The API is insanely simple:

bot.banlist.add( 617762 ); // :(
bot.banlist.add( 1337 );

bot.banlist.contains( 617762 ); //true
bot.banlist.contains( 1337 ); //true

bot.remove( 617762 ); // :D
bot.banlist.contains( 617762 ); //false

After that, bot.banlist[1337] looks like { told : false }. In case you're wondering why it's not just a boolean value, it's so you can do if (bot.banlist[id]) { ... } and it'll work.

Memory

An abstraction over persistent memory. You'll mainly want to use get and set. save is called automatically. Yeah...this is a TODO, I got bored.

saveInterval

data

get

set

loadAll

save

saveLoop

(Internal)