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
totrue
. - Changes
invocationPattern
topattern
(it'sbeer!
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 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.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, thee
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 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.input
event, is called, and passed the message object. It validates it againstvalidateMessage
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 (seeprepareMessage
andMessage
below 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 (see
bot.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
callsadapter.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 )
- Sendsresp
to the room.reply( resp )
- Sendsresp
as a reply to the user (remember,msgObj
is our source).directreply( resp )
- Sendsresp
as a reply to the message.parse()
- Parsestext
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 )
- MapsuserName
to a user id.findUsername( id )
- Maps a userid to a username.codify
escape
link
get( 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)