Wiki

Clone wiki

LucidBot / Scripting

Scripting

With for example a MIRC-based bot, it's usually easy to change and add new scripts to handle commands and events and such. Since LucidBot is built in Java you can't really change the source code easily (unless you're willing to recompile the project yourself). However, the bot lets you handle commands and events by adding scripts. Today there's a lot of different languages with implementations for the Java platform, and the ones that support the Scripting Engine API can be used for the bot. The table below shows some of the options you have (the ones I've tested. There are more out there that I haven't).

Language Java platform implementation Artifact
Javascript Mozilla Rhino No download needed (built into the java runtime)
Ruby JRuby JRuby 1.74 Complete (includes basic gems)
Python Jython Jython 2.7beta1 Standalone (includes standard libs)
Groovy Groovy Groovy (the binary download contains an embeddable folder with the groovy-all jar to use)

How to do it

We'll start with going through the basic steps, and then you'll get an example in python that shows how to combine everything into something that works.

Download the scripting engine

The first step is to download the scripting engine for the language you wish to use and put it in the scriptengines-folder inside the bot's folder. If you're using javascript you don't need to do anything here, since support is built into the java platform already.

Creating a script

When it comes to scripting, you currently have two options regarding what to handle. You can either handle commands or listen to events.

Handling a command

To handle a command, you need your script to implement an interface so that the bot knows what to do with the script. You have two choices here depending on how you want to handle the command parameters. The first option is to implement ScriptCommandHandler, which means you need to implement the following two methods:

Command handles() throws Throwable;

CommandResponse handleCommand(final IRCContext context,
                              final Params params,
                              final Collection<Filter<?>> filters,
                              final DelayedEventPoster delayedEventPoster) throws CommandHandlingException;

The above option forces you to deal with parsing the incoming command parameters yourself, because you haven't told the bot which parameters you expect the command to have. In this situation the bot will itself create two different parsers for the command. One that accepts no parameters at all, and one that simply accepts everything. The Params object is what holds what was parsed, so it's either not going to contain anything (if no parameters were used) or one single parameter called 'params' that holds a String that is the whole input (except from the command name itself).

Another option is to actually tell the bot how to handle the parsing by implementing ParameterizedScriptCommandHandler instead. It has the same methods as ScriptCommandHandler, but it adds an extra one too:

CommandParser[] getParsers() throws Throwable;

Here you send an array of CommandParser objects that the bot can use to parse the command, allowing you to actually make better use of the Params object by fetching one parameter at a time and such. It should become clear in the examples below how this works.

After you've added your script to the scripts-folder, you also need to create and add a template to handle the output from the command, just like for the built in bot commands. Once you've done that you're done. The bot will pick up your script as it boots, and you'll be able to call it just like any other command.

Handling an event

Events are different types of things that happen as a result of people interacting with the bot. The basic types of events are that someone types something in the channel that the bot can see. Other events are further along in the chain of the bot handling something, such as events being posted after a spell has been saved by the bot and such.

  • Here are some basic bot events.
  • Here are irc related events.
  • Here are utopia related events.

Handling an event requires you to implement the ScriptEventListener interface, which requires you to handle the below two methods.

Class<?> handles() throws Throwable;

void handleEvent(Object event) throws Throwable;

It's pretty simple really. You tell the bot which type of event you want to handle, and then it will call the script everytime that event is fired.

Examples

Simple example in JRuby

This example uses JRuby in the simplest possible way to handle a command. It shows how to use the ScriptCommandHandler interface to respond to a command without caring about the parameters.

See file here

require 'java'

def handles()
  Java::api.commands.CommandBuilder.forCommand("jruby").build()
end

def handleCommand(context, params, filters, delayedEventPoster)
  Java::api.commands.CommandResponse.resultResponse "helloWorld", "Hello World from JRuby"
end

Along with that is a template called jruby.ftl (same name as the command, as defined by the handles() method above):

See file here

<@ircmessage type="reply_message">
${helloWorld}
</@ircmessage>

Slightly more involved example using Javascript

This example instead uses Javascript, and it cares about parameters, using ParameterizedScriptCommandHandler to let the bot help with parsing them.

See file here

importPackage(Packages.api.commands);

function handles() {
    return CommandBuilder.forCommand("js").build();
}

function getParsers() {
    var parsers = java.lang.reflect.Array.newInstance(CommandParser, 2);
    parsers[0] = CommandParser(ParamParsingSpecification("name", "[^ ]+"));
    parsers[1] = CommandParser.getEmptyParser();
    return parsers;
}

function handleCommand(context, params, filters, delayedEventPoster) {
    var name = "world";
    if (params.containsKey("name")) name = params.getParameter("name");
    return CommandResponse.resultResponse("helloWorld", "Hello " + name + " from javascript");
}

As you can see, this example instructs the bot that there are two possible options for parameters. Either nothing at all, or one parameter that doesn't contain any spaces that we choose to call name. The command we're handling is called js, and as you can see, it uses the param object to extract the name variable (if it exists).

The template for this command looks exactly the same as the one for the ruby example above, it's just in a file called js.ftl instead.

Advanced example using Jython

In this example we're going to handle a command that requires us both to access data in the database, store data in a file and populate the data by listening to an event. We're going to listen to what people are typing on IRC and keep check of how many times each user types 'wtf' somewhere. Our command will then be able to show how many wtf's any given user is currently up to.

Our first step is to create a folder and a file where we can keep our data. Inside the scripts folder, we create a folder called data. In there we create a simple wtfs.txt file in which to put our data. You'll see the code below referring to this file.

Let's begin with the event listener:

See file here

#Works with Jython2.7b1
import re
from api.events.bot import NonCommandEvent


def handles():
    return NonCommandEvent #we want to get all the posts users make that aren't commands


def handleEvent(event):
    eventContext = event.getContext()
    postFromIrc = eventContext.getInput()

    if "wtf" in postFromIrc.lower():
        nickOfPostingUser = eventContext.getUser().getMainNick() #note that the difference between getUser and getBotUser is that the former represents the user on IRC, so it only has basic data like current nick and such, but that's all we need here
        add_wtf_for_user(nickOfPostingUser)


#As this example is mostly for show, the below isn't really perfect. We don't lock access to the file, so theoretically someone else could
#modify the file in between the read and write in this method, and then we'd lose one or more wtfs
#The code is also naive and could be simplified a lot by using something like 'pickle' probably
def add_wtf_for_user(userNick):
    pattern = re.compile(userNick + "\t(\\d+)") #regular expression for matching the line for the user
    file = open("./scripts/data/wtfs.txt", "r")
    contents = [] #save a list of all the entries from the file, so we can write it back later
    userFound = False
    for line in file:
        m = pattern.match(line) #check if this line matches for the user
        if m is not None: #yes, it's a match!
            updatedWtfs = int(m.group(1)) + 1 #add 1 to the existing count
            contents.append(userNick + "\t" + str(updatedWtfs)) #add line to the list
            userFound = True
        else:
            contents.append(line) #it wasn't the user we were looking for, so just add the line unmodified
    if not userFound: #the user isn't in the file yet, so we add a new line for him/her
        contents.append(userNick + "\t1")
    file.close()

    #write all the lines back to the file
    file = open("./scripts/data/wtfs.txt", "w")
    for line in contents:
        file.write("%s\n" % line)
    file.close()

So, we're listening to NonCommandEvent, which is everything that users are typing that isn't bot commands. If we run into some message containing 'wtf' we update a counter for the user in a text file.

Next up we need a command handler that lets us view the data we've saved with the listener. It looks like this:

See file here

#Works with Jython2.7b1
import jarray
import re
from api.runtime import ServiceLocator
from api.database.daos import BotUserDAO
from api.commands import CommandBuilder, CommandResponse, CommandParser, ParamParsingSpecification


def handles():
    return CommandBuilder.forCommand("wtf").ofType("lol-commands").build()


def getParsers():
    emptyParser = CommandParser.getEmptyParser() #this parser will catch the case where no parameters are used, just !wtf

    userParamParsingSpec = jarray.array([ParamParsingSpecification("user", "[^ ]+")], ParamParsingSpecification) #will catch !wtf <user nick>
    specificUserParser = CommandParser(userParamParsingSpec)
    return jarray.array([emptyParser, specificUserParser], CommandParser) #convert to a java array CommandParser[]


def handleCommand(context, params, filters, delayedEventPoster):
    if params.containsKey("user"): #if the user was specified, look it up in the database
        userdao = ServiceLocator.lookup(BotUserDAO) #get one of the bot's DAOs (data access object) to help look for the user in the database
        user = userdao.getUser(params.getParameter("user")) #get the user, or None if it doesn't exist. This method looks for all nicks
        #if we want fuzzy matching of user nicks: user = userdao.getClosestMatch(params.getParameter("user"))
        if user is None: #if no such user exists, return an error message
            return CommandResponse.errorResponse("No such user exists")
    else:
        user = context.getBotUser() #if no user was specified we use the calling user, which we can fetch from the context

    wtfs = get_wtfs_for_user(user.getMainNick()) #check how many wtfs this user has posted

    return CommandResponse.resultResponse("wtfs", wtfs, "user",
                                          user) #return data for the template to work with. In this case we send the user object and the wtfs


def get_wtfs_for_user(userNick):
    pattern = re.compile(userNick + "\t(\\d+)")
    with open("./scripts/data/wtfs.txt", "r") as source_file:
        line = source_file.readline()
        m = pattern.match(line)
        if m is not None:
            return m.group(1)
    return 0

The interesting thing going on here that isn't in any other example is the part where we're looking for users in the database. To accomplish that we're using on of the bot's built in classes, which we can fetch an instance of through the ServiceLocator class. The ServiceLocator class only has a single method, and you can use it to get an instance of any class the bot knows about. The DAO objects will probably be the most common targets, as they help with fetching data from the database.

The last piece of the puzzle is the template, which looks like this (in wtf.ftl):

See file here

<@ircmessage type="reply_message">
${user.mainNick} has said wtf ${wtfs} times!
</@ircmessage>

As you can see, it uses both the parameters from what the command handler returned when it handled the command (user and wtfs).

Updated