In todays episode of demonastery.org I'm going to build yet another IRC bot!

For this series however I am going to focus on the idea of decoupling. That is to say, separating out the functionality of the bot from the transports by way of a message queue (in this case we will be using ZeroMQ). By the end of this three part series we will have a bot that is mostly decoupled, allowing functionality and transports to be added and removed on the fly. Each part of this bot can run on separate machines and will be written in different languages!

DCBot

This part of the series will focus on the Core of the bot. This core is responsible for receiving messages from the transports (ie. user input) and forwarding those on to the other modules in order to generate a response. It will then forward any replies back to the transports to be sent back to the user. Today we will write this in Python (as a break from the usual PHP), although any language with 0MQ bindings should suffice.

To begin with, make sure you have 0MQ installed along with the Python bindings. We're going to use JSON to configure the core so make sure to also grab the simplejson module if you're using Python 2.6+.

We'll start by defining the ports that 0MQ will use to exchange information between the components. Place the following into a new file named core.json:

{
  "listen": "tcp://*:5555",

  "plugins": [
    { "name": "Redmine", "connect_string": "tcp://localhost:5556" }
  ]
}

With our config defined we can now start on the core code. Create a new file named core.py and start with the following contents:

#!/usr/bin/env python2
import simplejson as json
import zmq

# Core configuration.
fp = open("core.json", "r")
config = json.load(corefp)
fp.close()

Here we have imported the required modules and loaded the JSON into a config variable that we will reference in the rest of the code.

Next we will set up 0MQ and connect to the defined plugins. Note that 0MQ doesn't actually make any connections until attempting to send:

# Create a ZeroMQ Context.
context = zmq.Context()

# Create a REPLY type listening socket.
# We use this type by calling recv()
# followed by a send().
core = context.socket(zmq.REP)
core.bind(config["listen"])

# Initialize plugins array.
plugins = config["plugins"]

# Connect to all plugins (ie. functionality).
for plugin in plugins:
  # Create a REQUEST type socket.
  # This is the opposite of the socket above.
  # We use send() followed by a subsequent recv().
  socket = context.socket(zmq.REQ)
  socket.connect(plugin["connect_string"])
  plugin["socket"] = socket

With this out of the way, we can now define the logic of our core:

# Loop forever.
while True:
  # Receive a message from a transport.
  # Transports send to us so we don't need to know of them in advance.
  # Deserialise the JSON message.
  request = json.loads(core.recv())

  # Initialize payload (this contains the messages returned to the transport).
  payload = []

  # For each plugin.
  for plugin in plugins:
    # Send the message to this plugin.
    plugin["socket"].send(request["msg"])
    # Receive zero or more lines from this plugin and add them to the payload.
    lines = json.loads(plugin["socket"].recv())
    for line in lines:
      payload.append(line)

  # Send the payload back to the original transport.
  core.send(json.dumps(payload))

This is it for the core. It is currently useless without a transport and some functionality to go with it, although if you run it now it should not give you any errors.

I will cover two example transports (a simple REPL, and IRC) in the next part on Wednesday, followed by some example functionality (Redmine task info) in the final part on Friday, by which time we will have a fully functioning info bot. Thanks for reading!

P.S. I don't write a lot of Python so any improvements on my code would be highly welcomed.