Building a decoupled IRC bot. Part One: Transports
First rule of writing blog posts, don't give yourself arbitrary deadlines! :)
Welcome to part two (well, "One" because we started at "Zero") of this series. One thing I realised after writing the first part is that I didn't really provide a definition of decoupling (which is kind of important).
What is Decoupling?
When referring to software, it is the process of removing coupling or dependencies between individual parts of a system. When decoupling is achieved correctly, the failure of one part of a system should not result in the other parts failing.
The bot that we are implementing is only loosely decoupled, in the sense that one part failing won't cause the bot to lose a connection (unless the transport is the part failing of course).
Without further ado..
The IRC Transport
The job of the transport is to provide an interface to the user for interacting with our bot. This can be just about anything that is able to take a line of input, and reply with multiple lines.
When the transport receives a line from the user, it sends this to our bot's core (which then routes it to the scripts). The bot core then replies with any lines that are returned from the scripts. Quite simple really!
The first transport we're going to implement for our bot is an IRC transport, because it's simple to implement while being useful. I'm using my ZaneA/ProtoIRC PHP framework to simplify this even further, and so this will be required (all you need is the single protoirc.php
file from the repo). In a new file named irc_transport.php
, place the following:
<?php
require_once 'protoirc.php';
// Define the core's address.
define('DCBOT_CORE', 'tcp://localhost:5555');
// Create a ZeroMQ context.
$context = new ZMQContext();
// Set up the core socket. This is a REQUEST type socket.
$core = new ZMQSocket($context, ZMQ::SOCKET_REQ);
$core->connect(DCBOT_CORE);
// Connect to the IRC server and channel.
$irc = new ProtoIRC('DCBot@chat.freenode.net:6667/DCBot');
// Listen for messages in the channel. Refer to the ProtoIRC page for more info.
$irc->in('/^:(.*)!.* PRIVMSG (.*) :(.*)/', function ($irc, $nick, $channel, $msg) use ($core) {
// Send the received message to the core to be processed by the scripts.
$core->send(json_encode(array('type' => 'message', 'nick' => $nick, 'msg' => $msg)));
// Wait for and receive a list of messages to send back to the user.
$out = json_decode($core->recv());
// Foreach line received, prefix it with the users nick.
foreach ($out as &$line) {
$line = "{$nick}: {$line}";
}
// Send the lines to the user. ProtoIRC takes care of sending them one at a time.
$irc->send('#DCBot', $out);
});
// Start the bot.
$irc->go();
Hopefully this is fairly straightforward, and when run should connect to your IRC server/channel of choice.
The REPL Transport
This transport is a plain local REPL written in Chicken Scheme. It simply takes a line of input and sends it to the core for processing. It requires the zmq
module which you can install using chicken-install zmq
. You'll probably want to do the same with the json
module if you don't already have it. In repl_transport.scm
:
#!/usr/bin/csi -s
; Import wanted modules.
(use (prefix zmq zmq:) json)
; Define the core's address (you may note the similarity between this and the PHP version!).
(define *dcbot-core* "tcp://localhost:5555")
; Create a ZeroMQ context and core socket.
(let* ((context (zmq:make-context 1))
(core (zmq:make-socket 'req context)))
; Connect to the core.
(zmq:connect-socket core *dcbot-core*)
; Read a line.
(let loop ((line (read-line)))
; Send this line to the core.
(zmq:send-message
core
(with-output-to-string
(lambda ()
(json-write
(alist->hash-table `((type . "message")
(nick . #f)
(msg . ,line)))))))
; Receive the lines from the core, and print them.
(with-input-from-string (zmq:receive-message core)
(lambda ()
(for-each print (json-read))))
; Loop.
(loop (read-line))))
This one, when run should provide a very basic feedback loop for testing out the bot. Plus it's an excuse to have multiple languages in one post!
Conclusion
We have now defined the transports for our bot, but as of right now the bot will never respond with anything! That is because the scripts that provide functionality are still missing. This will be resolved in the next part, concluding the series :)
Please feel free to comment and give me any feedback that you have as it will help me to improve my writing in the future. Thanks for reading!