Wiki
Clone wikiLucidBot / 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.
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.
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):
<@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.
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:
#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:
#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):
<@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