Writing a Halibot Module

Wed 22 April 2015 by Eric Richter

For those who are unfamiliar with Halibot, it is a modular XMPP Bot that I wrote in Python. It can be found on Github here, with slightly more information on how it functions. I will go into more detail maybe in a future post on the design, but this post will focus on developing a simple module to demonstrate its simplicity, and highlight some of its functionality. A working knowledge of Python 3 is recommended for this, as well as familiarity with JSON.

Module Structure

Directory

Before going into code, first allow me to explain the structure of a module, and the modules directory. Halibot expects all modules to be in a specific modules/ directory, and each in their own subdirectory. So, to start this tutorial, create a subdirectory inside of modules called hello.

$ cd modules/
$ mkdir hello

module.json

Inside of this directory, Halibot expects there to be a file called module.json. This file will defines what files provide what module. See, these directories are actually a "Module Group", as it can provide more than one module to Halibot, in different files.

The following is a bare bones module.json file:

{
    "modules": [
        {
            "name": "Hello",
            "path": "hello.py"
        }
    ]
}

Things to point out here: the modules key refers to a list of objects corresponding to each module this group exposes. In each of these objects, a name and path key is required. The name key should refer to the name of the module that will appear in the modules list, and by convention should also correspond to the object name that inherits from the XMPPModule class. path should be the path to the python source file that contains the XMPPModule-inheriting class.

So, the above example file states that there is only one module provided in this directory, called Hello and contained in hello.py. It is important to point out that the path is relative to the module's directory, so the actual full path is modules/hello/hello.py.

Other Files

If you want to seperate your module code from other logic, you use multiple files and import them like any other Python source. An example of this can be found in the Halibot-module implementation of the IRC game Acronyms here.

Module Code

Halibot modules have a small amount of boilerplate require by design. Halibot expects an object that implements the XMPPModule class, and that's about it. So the required boilerplate is the following:

from module import XMPPModule

class Hello(XMPPModule):
    pass

To start receiving messages, simply implement the method handleMessage in this class. Consider the following sample code:

class Hello(XMPPModule):

    def handleMessage(self, msg):
        self.xmpp.reply(msg, "You said " + msg["body"])

A few important things to point out from this example: - msg argument - this is an unmodified message object generated from SleekXMPP. It is simply a dictionary, and refer to their documentation for usage. - self.xmpp - this is a reference that is part of the XMPPModule class that refers to the bot object itself. Use this to interact with the bot, including sending messages. - reply - A convenient method implemented in the bot to reply to a message object. Takes in a message object, and a string to send.

This sample code will work for both single user chat and multi-user chats (MUCs), because handleMessage is called by default by both handlers. This makes things simple if both cases should be handled similarly. reply was implemented with the same thought, if the message object came from a MUC, then it will send the string in the second argument back to the same MUC.

Back to the example: What does this module do? Every time a user sends a message to either the bot individually, or in a MUC that the bot is it, the bot will respond with "You said " followed by a repeat of the message. Anything you do in the handleMessage method is just Python, so you could easily implement a command structure by checking the content of the message, for example. Let's say we only want to respond if someone says "!hello".

from module import XMPPModule
import halutils

class Hello(XMPPModule):

    def handleMessage(self, msg):
        cmd, args = halutils.splitArgList(msg)
        if cmd == "!hello":
            self.xmpp.reply(msg, "Hello, {}!".format(halutils.getSensibleName(msg)))

Wait, what's this halutils thing? It is a library provided to make managing the message objects simpler, and a bit more abstract. So, splitArgList will return a tuple of the first word (assuming separated by space), and a list of the rest of the words. In the previous example, receiving a message "!hello world foo" would return the following tuple: ("!hello",["world", "foo"]). Next, we compare against the string, and conditionally reply if the command (first word) is "!hello". This time, we reply with the senders name, as provided by getSensibleName. This function returns a string with exactly that: a sensible name. If the message was received from a MUC, then it will return that user's nickname, otherwise the JID without the "@xmpp.server.net" bit.

Conclusion

That's it! Module development is pretty simple, and does not require much extra effort. In most cases, only a little extra work is required, and the XMPPModule and halutils library does most of the heavy lifting. I may continue this series in the future with more advanced module development, but for the most part, the module.py file in the source is fairly self-documenting. Considering taking a look at some of the reference modules here other examples of what can be done with the API.


Fork me on GitHub