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,
devModetotrue. - Changes
invocationPatterntopattern(it'sbeer!by default because of...uh...historical reasons). - Removes all
IO.events.userjoinlisteners (so we won't have any duplicate user join/leave things). - Changes
validateMessageto only check for theinvocationPattern.
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.ondatais 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, theeproperty holds an array of all the events which happened at the room. For every such event,adapter.in.handleMessageObjectis called, passing along the event. -
handleMessageObjectdoes the base event handling: If it's a user-related event (user joined, user left), it callshandleUserEvent, which just raises the appropriate event. It also possibly handles multiline messages, and finally callsIO.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 theIO.events.inputevent, is called, and passed the message object. It validates it againstvalidateMessageto 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 (seeprepareMessageandMessagebelow for details), followed by a ban check (should we respond to that user?). Finally, we act on the message contents: If it's aneval, 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.addwith the output and the roomid. -
At the final stage of our journey,
adapter.out.sendcallsadapter.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 )- Sendsrespto the room.reply( resp )- Sendsrespas a reply to the user (remember,msgObjis our source).directreply( resp )- Sendsrespas a reply to the message.parse()- Parsestextas 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 )- MapsuserNameto a user id.findUsername( id )- Maps a userid to a username.codifyescapelinkget( key )- Gets a property frommsgObj, useful for getting the userid and whatnot.set( key, val )- Sets a property onmsgObj.
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)