The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

How to write a conduit

CONSIDERATIONS

A conduit is a method for getting queries in to the infobot, and spitting out replies. The most obvious is an IRC server, but there's no reason that any two way data source can't be turned in to a conduit.

infobot uses POE to avoid blocking between various IO requests. While the code is written in such a way as to hide POE as far as possible from developers building simple plugins, building a conduit will definitely require it, so some familiarity with POE is required for this tutorial.

A BAD IDEA

Wouldn't it be great to be able to interact with infobot via email?

SCAFFOLDING

infobot makes some assumptions about your components. The first of these is that you have a load() method which returns true or false depending on if the system is capable of running your component. In almost every case, this involves checking for the availability of modules.

We're going to be using Email::Folder, Email::Delete, Email::Address and Email::Send. Luckily, Infobot::Base has a method to make it very easy to check for required modules, and what's more, it'll autmoatically check your package for an array containing these modules. Even better, it's called load(), so you don't even need to define load() in most modules.

FINALLY, POE is definitely available to you, and will export some useful stuff in to our namepace, so we use it explicitly.

Hence:

 package Infobot::Conduit::Mail;

   use strict;
         use warnings;

 # Import new() and a useful load()

   use base qw( Infobot::Conduit ); # Specialised subclass of Infobot::Base

 # Modules we'll be needing

   our @required_modules = qw(Email::Folder Email::Delete Email::Send Email::Address);

 # Load POE explicitly

   use POE;

CONFIGURATION

Our conduit is going to need some external data to set up properly - an email address and name to send from, a subject line to use, an incoming mailbox to monitor, AND a frequency for monitoring. And we're going to want this from the configuration file that runs infobot.

The convention for adding modules in to the configuration file is nice and simple. This is a conduit, so it sits under the conduit section. We need to define a class for it, and any extras we like. An example will make this clear:

 conduit:
    'Bad Idea Email':
      class : Infobot::Conduit::Mail
      extras:
          server    : localhost
          from      : 'infobot <infobot@clueball.com>'
          subject   : Infobot Reply
          mailbox   : /home/sheriff/Maildir/infobot/
          frequency : 10 

This is YAML. It's a bitch with whitespace being non-perfect, so be careful. Bad Idea Email is how we talk about the component, class is the package which provides it, and you freestyle the extras to whatever you want.

At this point, we almost have a working component. All that's left is ...

INIT

Components are given a chance to do any set up once they're loaded in their init method. The init method is passed the name of the component (so: Bad Idea Email in this case), and is expected to return 1 on success.

You can use this name, to access the configuration values you set. There's a long way and an easy way. We're interested in the easy way:

 $self->set_name( shift() );

This sets <$self-{name}>> appropriately, and makes everything from extras available in <$self-{config}>>.

So to make this a workable module, let's add a very simple init() method which doesn't do anything... So that we get some output, we're going to write to the log. The log is available through any subclass of Infobot::Base as log. Pass it a priority and a message - you can find a list of priorities in Infobot::Log. We're going to set our priority to 2 - a serious error - just so it shows up:

 # Setup

 sub init {

   my $self = shift;

   $self->set_name( shift() );

   $self->log( 2, "We will be reply to mail as $self->{config}->{from}" );

   return 1;

 }

Add it in, fire up infobot! Amongst other lines, I get:

 [Infobot] 4. Loading conduit [Bad Idea Email] [Infobot::Conduit::Mail]
 [<::Base] 2. We will be reply to mail as Infobot <infobot@clueball.com>

INPUTS

How to get email in to the infobot? We're going to go with polling a mailbox every so often, and using the subject lines of any mail we find as queries, and then delete the email.

To do this nicely, every x seconds, without blocking, we're going to use POE. As this tutorial requires a good knowledge of POE, we'll be skipping over how it works... What we do need, is to set up a session in our init block, and post our first event to it...

 # Setup

 sub init {

   my $self = shift;

   $self->set_name( shift() );

   POE::Session->create(

     object_states => [
       
       $self => [qw( poll _start )],

     ]

   );

   return 1;

 }

So far so good. We're now going to need to define a _start method, and a poll method - the method that we're going to set up to run periodically. _start just needs to call poll for the first time:

 sub _start {

 # We'll give everything 15 seconds to load... Remember $poe_kernel
 # has been provided by the POE module automatically...
   
   $poe_kernel->delay_set( poll => 15 );

 }

The poll method is a little more complicated, so let's just make it print a message for the time being...

 sub poll {

   my ( $self ) = $_[ OBJECT ];

   $self->log( 2, "Polling" );

   $poe_kernel->delay_set( poll => $self->{config}->{frequency} ); 

 }

And let's set it to run! I get the output:

 [<::Base] 2. Polling
 [<::Base] 2. Polling
 [<::Base] 2. Polling

Great news!

CREATING A MESSAGE

This document isn't about using the Email modules, so you'll just have to trust the following routine rewrite of poll()that you need to add, blindly. This will delete all mail it finds in the target mailbox.

 sub poll {

   my ( $self ) = $_[ OBJECT ];

   my $folder = Email::Folder->new( $self->{config}->{mailbox} );

   for my $email ( $folder->messages ) {

                        my ($address) = Email::Address->parse( $email->header('from') );

     $self->process( 
        name  => $address->phrase,
        email => $address->address, 
                                text  => $email->header('subject')
                        );

                        my $message_id = $email->header('Message-ID');

                        Email::Delete::delete_message(
                                from => $self->{config}->{mailbox},
                                matching => sub {
                                        my $message = shift;
                                        $message->header('Message-ID') =~ $message_id;
                                }
                        )

   }

   $poe_kernel->delay_set( poll => $self->{config}->{frequency} ); 

 }

Let's write a light-weight process method at this point so we can see this code in action.

 sub process {

   my $self = shift;
                my %options = @_;

        $self->log( 2, "$options->{name} from $options->{email} said $options->{text}" );

 }

And run it! You'll need to arrange for some mail to find its way in to your infobot mail store for this to happen tho...

CREATING A MESSAGE

SAY