IMPyBot: An XMPP Bot and Plug-in Framework in Python

Kefei Lu, klu1024 AT gmail.com
01/26/2010

Please visit the project website for more information: http://impybot.sourceforge.net/

1. Introduction

IMPyBot (impybot) is a bot (IM automatic response) framework for XMPP protocol. It provides a plug-in system, so that the implementation of a utility is isolated from implementation of the bot. This provides great convenience to manage the utilities without touching the bot itself. The plug-in system is sophisticated that it enables developer to write really large and powerful plug-ins. Meanwhile, it also provides a simple interface such that users with little Python experience can write useful utilities as well.

The figure below illustrates the structure of IMPyBot framework. As shown, the plug-ins are isolated from the bot itself. Plugin developers who are only interested in writing utilities like weather querier or calculators does not need to know the details of the bot at all. What is more important, this provides an easier way to organize and manage all the plugins.

     +------------------------------------------------------------+
     |                                                            |
     |                     The Plug-ins                           |
     |       +--------------------------------------------+       |
     |       |  +---------+ +------------+ +------------+ |       |
     |       |  | weather | | dictionary | | calculator | |       |
     |       |  +---------+ +------------+ +------------+ |       |
     |       +--------------------------------------------+       |
     |                            |                               |
     |                            | plug in                       |
     |                            v                               |
     |       +--------------------------------------------+       |
     |       |         The XMPP Bot Framework             |       |
     |       +--------------------------------------------+       |
     |                           | ^                              |
     |                           | |                              |
     |                           v |                              |
     |       +--------------------------------------------+       |
     |       |                 Internet                   |       |
     |       +--------------------------------------------+       |
     |                                                            |
     |                                                            |
     +------------------------------------------------------------+
            Figure 1. Illustration of the IMPyBot Framework

2. Installation

  1. Download the package from https://sourceforge.net/projects/impybot
  2. Unzip the package.
  3. Install to the system.

2.1. Use Without Installation

You can also use IMPyBot without installing it to the system location.

  1. Download the package from https://sourceforge.net/projects/impybot
  2. Unzip the package.
  3. Open a terminal inside the unzipped directory, do:
      run_bot.py -j "username" -p "password"
    

3. A Guide for The Impatients

To write your own utilities (we call it a plugin), simply follow these TWO steps:

3.1. Write a plugin module

Create a text file called my_first_plugin.py, with codes look like the following:

 1 import impybot  # make sure it's properly installed
 2
 3 class MyFirstPlugin(impybot.SimplePlugin):
 4
 5     # If a message has lines begin with ANY of these words,
 6     # handle_match() will be called.
 7     command = ('echo', 'display')
 8     
 9     def handle_match( self, matched, sender_jid ):
10         msg = 'The strings after the matched commands:'
11
12         # If a line in a message matches ANY words in ``command'',
13         # the string after the matched word of that line goes into
14         # the ``matched'' tuple.
15         for m in matched:
16             msg = msg + '\n' + m
17
18         # The returned string will be sent back as a reply.
19         return msg
20
21 # DON'T FORGET TO REGISTER THE CLASS!!!
22 impybot.register(MyFirstPlugin)

3.2. Invoke the bot

Invoke the command line tool to run a bot and tell it where your plugin is:

  python -m run_bot -j "jid@server.com" -p "password" -m my_first_plugin.py

3.3. That's it!

Congratulations on your first Instant Messaging Application!

4. A Guide to the IMPyBot Framework

4.1. Making the Bot Running & Configuring the Bot

IMPyBot can be invoked from the command line through the application run_bot.py. To simply start the bot, use the following command:

  python run_bot.py -j "your_id@server.com" -p "your_pswd"

Then the bot will start with the account and password you specified. For detailed usage of run_bot.py, invoke it with the -h options:

  python run_bot.py -h

Besides using the command line to configure the bot, system wide and user level configuration files are also accepted. The following table summarizes the name of configure files used on different operation systems.

Windows Unix/Linux
System None /etc/impybotrc
User %USERPROFILE%\impybot.ini ~/.impybotrc

The configuration file is XML formated and should looks like the following:

 1 <impybot>
 2     <auth jid="" password="" server="" port="" />
 3     <proxy host="" port="" user="" password="" />
 4
 5     <plugin>
 6         /path/to/plugins_1
 7     </plugin>
 8
 9     ...
10
11     <plugin>
12         /path/to/plugins_n
13     </plugin>
14
15     <plugin_store>
16         /path/to/plugin/store_1
17     </plugin_store>
18
19     ...
20
21     <plugin_store>
22         /path/to/plugin/store_m
23     </plugin_store>
24 </impybot>
  1. In the tag auth, attributes jid and password specifies the JID and password the bot uses to log on. Attributes server and port specifies the alternative IP and port to be used and are optional.

  2. The tag proxy specifies the proxy the bot should use. It is also optional.

  3. The tags plugin corresponds to the command line option -m, and are used to specifies the paths to the modules and packages in which the plugins are defined. plugins are optional.

  4. The tags plugin_stores corresponds to the command line arguments. They are used to specify the directories where the plugin modules and packages are stored. plugin_storess are optional.

4.2. Why My Bot Does Nothing After Being Invoked?

One would notice that the bot, although having been running, actually will do nothing to the incoming events such as users' chat activities, status changes. This is because, designed with "Keep It Simple, Stupid" (KISS) in mind, the bot itself does nothing except handling the connections. All actions that depend on specific incoming message patterns, status changes, are implemented by the plugins using the module's powerful and easy-to-use plugin system (detailed in the next section). By splitting various functionalities from the implementation of the bot, it is easy to manage these functionalities, it is also easy for developers who are only interested in implementing such functionalities.

Assume that we already have some plugins available in a directory /path/to/plugin_stores/. The plugins in this directory include:

  1. weather.py: a weather query utility.
  2. dict.py: a dictionary look up utility.
  3. calculator.py: a calculator utility.

How do we tell the bot to use these plugins when we invoke the bot? Simply pass plugin_stores to run_bot.py:

  python run_bot.py -j "jid@srv.com" -p "pswd" /path/to/plugin_stores

You can pass as many plugin stores as you want to run_bot.py.

Now when some user send a message to jid@srv.com like:

  weather Miami, FL

The bot will invoke the weather plugin, retrieve the weather data for Miami, Florida, and then send it back to the user.

4.3. How Does the Bot Invoke the Plugins?

After initialization, the bot looks for the paths of all known plugins, imports their containing modules or packages, and registers them with the specified priorities. When an event (an incoming message, or a status change) comes, the bot invokes the plugins one-by-one in the order specified by the plugins' priorities. Each plugin has a chance to be executed for the event, and the plugin itself has the right to decide whether or not an action should be performed on it. If non of the plugins decides to take an action on the incoming message, the bot will invoke the built-in fallback plugin for the message. By default, it will tell the sender of the message that it cannot understand what he/she is saying.

Note that a plugin, in the case that it takes an action on the incoming message, has the right to tell the bot to stop invoking the plugins with lower priorities, which haven't had a chance to be invoked by the bot. This "stop request" is only valid during this "plugin-execution round" and this event, and is not valid for the following events.

The "stop request" feature is for advanced plugins only and in most cases should not be used.

5. A Guide to the Plugin Framework

Most users of IMPyBot might be merely interested in writing their own utilities. In IMPyBot framework, this is done by writing a "plugin".

IMPyBot provides a powerful yet easy-to-use plugin framework. Even some of the bot's built-in functionalities are implemented as built-in plugins. Currently, IMPyBot has three types of plugin classes that can be used, each of which aimed for different purpose. The figure below gives an overview of these plugins and illustrates their relationships.

   +--------------------------------------------------------+
   |                                                        |
   |  +----------------------+    +----------------------+  |
   |  | impybot.SimplePlugin |    |   impybot.RePlugin   |  |
   |  +----------------------+    +----------------------+  |
   |              |                          |              |
   |              |                          |              |
   |              v                          v              |
   |  +--------------------------------------------------+  |
   |  |                impybot.Plugin                    |  |
   |  +--------------------------------------------------+  |
   |                                                        |
   +--------------------------------------------------------+
             Figure 2. Plugin system inheritance

As shown in Figure 2, impybot.Plugin is the base class for all plugins. It is the most general purpose plugin framework and provides the most flexibility to the plugin developers. impybot.SimplePlugin is a easy-to-use, command-based plugin. The developer simply specifies a sequence of command strings he/she wants to match, and a callback function that will be called when any of the command strings are matched. impybot.RePlugin is more advanced and flexible than SimplePlugin. It is regular expression based. The plugin developer specifies the regular expression pattern he/she wants to match and provides a callback function which will be called on a match.

5.1. An Example of the Use of SimplePlugin

The use of SimplePlugin is simple. The plugin developer only needs to derive a class from impybot.SimplePlugin and sets a few class attributes and a callback method. Next we will give an example of using SimplePlugin to echo users' inputs.

 1 # -*- coding: utf-8 -*-
 2
 3 import impybot
 4
 5 # Derive a class from SimplePlugin and name it to something
 6 # meaningful
 7 class EchoPlugin(impybot.SimplePlugin):
 8     '''An example use of SimplePlugin.'''
 9
10     # the "command" attribute is used to specify a list of command
11     # strings that you want to match on user inputs. If ANY string
12     # matches, the callback method will be called.
13     command = ('echo', u'hello', u'你好')
14
15     # the callback method. will be called everytime there is a match.
16     # "matched" is a list of matched strings following the command strings
17     # "sender" is the sender's email address (JID).
18     def handle_match(self, matched, sender):
19         reply = "\n".join(matched)
20         # the return value is a string, which will be forwarded to the sender
21         # (automatically!) as an instant message.
22         return reply
23
24 # IMPORTANT!!! DON'T FORGET TO REGISTER THE CLASS HERE!!!
25 impybot.register(EchoPlugin)

As an overview, there are only five things you need to do for writing such a plugin:

  1. Import the impybot module
  2. Derive a class from impybot.SimplePlugin
  3. Set the command attribute as a tuple of command strings you want to match
  4. Define handle_match(): the callback method.
  5. (IMPORTANT!) Register the class using impybot.register()

For the above plugin, if "tom.jerry@gmail.com" sends the bot the following instant message:

  echo
  echo arg1 arg 2
  hello world!!!
  你好 吗?

The bot will reply to him/her with the following message:

  (An empty line)
  arg1 arg2
  world!!!
  吗?

Now let's go through the codes and gives a more detailed explanation.

5.1.1. The ``command`` Attribute

The command attribute defined on line 13 specifies the command strings that can trigger the callback actions of this plugin. In the above example, the plugin's callback actions will be triggered if someone sends the bot a message with any of the strings in attribute command appears at the beginning of ANY line.

In the attribute command, you can use either the str type, or the unicode type. If only ONE command string are to be used, command can be that string in this case: a tuple or a list is NOT necessary.

Rule of A Match

There are two cases of matches:

  1. A line starts with one of the specified command strings, AND followed by one or more space characters, and OPTIONALLY some non-space characters.

  2. A line starts with one of the specified command strings, AND an immediately End-of-Line or newline character ("\n").

In other words, one of the command strings DIRECTLY followed by non-space characters will not match.

Note that a line with echo alone still matches. Also note that in 你好 吗?, the space is NOT an ASCII space. This also counts. Users from non-English world would be very happy with this feature!

5.1.2. The callback method: handle_match()

If the input hits a match, then the callback method handle_match() will be called by the bot framework. And the matched parts of the input and the sender's JID will be passed to this method. The interface of this callback method likes like:

  def handle_match( self, matched, sender )

Arguments to handle_match()

matched is a tuple of matched parts. Elements in matched tuple is a unicode string. For lines with only a command string, the corresponding matched elements is a unicode empty stirng. Note that for inputs like command arg1 arg2, the corresponding matched element is arg1 arg2 as one string. This is because IMPyBot does not know how you will use the matched part, thus it will not simply assume you want space-separated arguments.

sender is a unicode string, representing the message sender's "email address" (or, JID). For advanced users: sender will always be the bare JID, which means you cannot get the resource part from sender. If you need the resource part, access the raw message object (an xmpp.Message object) via self.__message and call its method getFrom().

The Return Value

The return value of the callback method will be sent back to sender. If you do not have anything to send, or you just want to ignore this input, simply return None or "", or just return.

There are two ways to represent what you want to send. A string or a unicode string is enough in most cases. But if you need to return a multi-line message, with string normally it is necessary to do something like:

  reply = []
  # do something...
  reply.append('something')
  # do something else....
  reply.append('something else')
  #finally:
  return "\n".join(reply)

Alternatively, you can use class self.Response:

  reply = self.Response()
  # do something...
  reply.append_line('something')
  # do something else...
  reply.append_line('something else')
  # finally:
  return reply

Yes! You can directly return a self.Response type object.

self.Response does not only consist of a couple of convenience methods. It exists because of an important reason - to tell the bot to stop matching other plugins with lower priorities. More on this later.

5.1.3. Register Your Plugin

THIS IS VERY IMPORTANT. After you finish writing your plugin class, don't forget to register it so that the bot will know of it and will execute it when there is a proper user input. The way to register your plugin is by calling the register() function:

  impybot.register(SomePluginClass, priority = some_priority)

Register your plugin class out of the class's scope. Or to say, the indentation of this line of code MUST be the same as where you declare your plugin class.

Normally the optional priority argument does not need to be worried - a default priority value will be assigned to your plugin. In case that you need your plugin to have higher or lower priorities, specify it here. A smaller integer indicates a higher priority - think it as "niceness". Plugins with higher priorities will be executed before ones with lower priorities.

The acceptable range of priorities are from 0 to 20. Don't use priorities out of this range. register does not allow it. And doing so may ruin your own bot applications.

Why Don't We Use Decorators

Advanced Python uses may be attemped to use class decorators for the registering purpose. However, considering the backward compatibility with lower versions of Python, I have to make the decision of not using decorators. I might later provide it as an optional feature.