Część 1

Mamy już szkielet bota, ale taki bot powinien być łatwy w rozszerzaniu a trzymanie wszystkiego w jednym pliku nie jest szczególnie czytelne, spróbujmy więc rozbić go na rdzeń i moduły dostarczające content.

bot.pl v0.0.2

Ścieżka do modułów + helper dostarczający wygodnego ‘load $module_name’

use lib './lib';
...
use Module::Load;

Tutaj wczytujemy listę modułów z configa i przekazujemy im parametry.
Przy okazji zapisujemy wynik metody ‘info’ która posłuży nam jako opis modułu w helpie

my $module = {};
while (my ($name, $module_config) = each %{ $cfg->{'modules'} } ) {
    my $modulename = 'XANi::Infobot::Agent::' . ucfirst($name);
    load $modulename;
    my $m = $modulename->new($module_config);
    $module->{$name}{'info'} = $m->info();
    $module->{$name}{'handler'} = $m;
}

Jak widać zamiast prostego “nazwa modułu = referencja do funkcji” hash ma w wartości oddzielne “pola” ( technicznie wartość jest referencją do hasha ) oraz przekazywana jest referencja do obiektu zamiast do funkcji. Taka konstrukcja zapewnia zarówno wygodniejszą obsługę ( można wołać funkcję przez bardziej intuicyjne $h->{‘cos’}->function() zamiast cudowania z nawiasami) jak i możliwość późniejszego rozszerzenia funkcjonalności, można np. zapisywać w tym samym miejscu ACLki “kto daną funkcję może wołać”

Teraz możemy już wywoływać moduł:

   message => sub {
      my ($cl, $acc, $msg) = @_;
      my ($target_module, undef) = split(/\s+/,$msg->any_body);
      if ( ! defined( $module->{$target_module} ) ) {
          &help($cl, $acc, $msg);
      }
      else {
          $module->{$target_module}{'handler'}->msg_handler($cl, $acc, $msg);
      }
   },

Nic specjalnego, wołamy moduł jeżeli istnieje, jeżeli nie wywołujemy funkcję help, która po prostu iteruje po liście modułów i formatuje output. Teraz czas zrobić moduł ;].
Zdecydowanie polecam tworzenie template modułu używając jednego z automatów jak tradycyjnego h2xs -XAn My::New::Module czy nowszego module-starter z libmodule-starter-perl, oba wygenerują odpowiednie makefile i template do pisania testów, ale od biedy wystaczy żeby plik zaczynał się od package Nazwa::Modulu i był w odpowiedniej ścieżce.

lib/XANi/Infobot/Agent/Echo.pm

sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self = {};
    bless($self, $class);
    return $self;
};

Standardowa inicjalizacja “obiektu”. Gdybym chcieli brać parametry to są one przekazywane tak samo jak każdej innej funkcji więc można zrobić

    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self = {};
    bless($self, $class);
    if (ref($_[]) eq 'HASH' ) {
        $self->{'config'} = shift;
    } else {
        my %cfg = @_;
        $self->{'config'} = \%cfg;
    }

i potem podawac parametry przez:

my $mod = Mod::Ule->new(
                        some_param => var,
                        debug      => 1,
);

albo podając referencje do hasha np. część configa:

my $mod = Mod::Ule->new(
                        some_param => 'var',
                        debug      => 1,
);

czy

my $mod = Mod::ule->new({debug => 1});

Niestety przez to że handlujemy zarówno hash jak i referencję do hasha kod jest trochę rozwlekły (jak na perla ;)), mieszane argumenty pozostawiam jako ćwiczenie dla czytelnika ;]. Ofc jeżeli wiemy że dane moduły zawsze będą wołane w ten sam sposób (tak jak w naszym przypadku), można zaprzestać na $self->{'config'} = shift

Ale idziemy dalej :

sub info {
    my $s = shift;
    return "Simple echo plugin";
};
 
sub msg_handler {
    my $s = shift;
    my ($cl, $acc, $msg) = @_;
    my $repl = $msg->make_reply;
    my (undef, $reply) = split(/\s/,$msg->any_body);
    $repl->add_body ( "Echo: " . $reply);
    $repl->send;
};

Tu po prostu copy-paste z poprzedniego kodu z dodatkiem my $s = shift jest to standardowy konstrukt przy “obiektowym” perlu, częściej nazywany jest $self.
Przez niego uzyskujemy dostęp do “reszty” obiektu np

$self->{'config'}{'debug'}

da nam dostęp do wczytanej wcześniej zmiennej a

$self->help;

wywoła funkcję help

I tyle wystarczy do stworzenia najprostrzego modułu. W takiej formie cokolwiek zrobisz w module co zablokuje proces (czekanie na IO czy na odpowiedź serwera) zablokuje inne requesty do bota, więc żeby w pełni wykorzystac potencjał event loopa należy stosowac “ueventowionych” funkcji jak nieblokujące operacje IO/DBI/HTTP, ale o tym w następnej części ;]