Das kommt ins Protokoll (Linux-Magazin, November 2009)

Totgesagte leben länger: IRC ist populär wie nie und lässt selbstgeschriebene ``Bots'' zu, automatische Helferlein, die Dienste verrichten und auf Teilnehmeranfragen antworten. Ein Logger-Bot protokolliert die Sitzung mit und archiviert sie in einer Datenbank.

Bei Konferenzen, im Support von Open-Source-Produkten und bei missionskritischen Aktionen, die die Koordination vieler Projektmitarbeiter erfordern, ist IRC nach wie vor die Nummer Eins unter den Instant-Message-Angeboten. Weder die proprietären Angebote von Yahoo oder Microsoft, noch das offene Google-Talk-Protokoll waren bisland in der Lage, den ehrwürdigen Dinosaurier der verzögerungsfreien Gruppenkommunikation aufs Altenteil abzuschieben.

Das in [2] schon einmal im Zusammenhang mit einem Hitzefühler kurz vorgestellte Modul Bot::BasicBot wickelt die Kommunikation zwischen Perl-Skript und IRC-Server so geschickt ab, dass zur Erstellung eines Bots wirklich weniger als zehn Zeilen Code notwendig sind. Das Modul Bot::BasicBot::Pluggable treibt das Konzept noch etwas weiter, in dem es selbstgeschriebene Bots mit Plugins ausstattet, die die Teilnehmer einer Chat-Session durch spezielle Nachrichten aktivieren können. Fühlt sich ein Plugin angesprochen, führt er die ihm gestellte Aufgabe aus und schickt die Antwort zurück in den Chat.

Verzweifelt gesucht

Dabei enthält das CPAN-Modul von Haus aus eine gute handvoll nützlicher, voll funktionsfähiger Plugins, die sich einfach über die load()-Methode aktivieren lassen. Listing botstart zeigt die vollständige Implementierung eines Skripts, das sich in einen IRC-Channel einklinkt und zwei verschiedene Plugins aktiviert.

Listing 1: botstart

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Bot::BasicBot::Pluggable;
    04 
    05 my $bot = Bot::BasicBot::Pluggable->new(
    06     channels => ["#perlsnapshot"],
    07     server   => "irc.freenode.net",
    08     nick     => "snapshot-bot",
    09 );
    10 
    11   # 'Seen' module: remembers when and where
    12   # participants were last seen
    13 $bot->load("Seen");
    14 
    15   # DNS module: responds to "nslookup"
    16   # messages by looking up IP addresses
    17 $bot->load("DNS");
    18 
    19   # Connect to IRC server
    20 $bot->run();

Der ``Seen''-Plugin merkt sich, welche Teilnehmer sich an- und abmelden und antwortet auf das Kommando ``seen username'' eines beliebigen Users, wann der Teilnehmer zuletzt gesichtet wurde (Abbildung 1). Das ist insbesondere hilfreich, falls der Teilnehmer sich gerade nicht im Chat aufhält. Der zweite von botstart aktivierte Plugin, ``DNS'', implementiert eine einfache Schnittstelle zum Unix-Kommando nslookup und antwortet auf Kommandos im Format nslookup hostname mit der vom DNS-Server zurückgelieferten IP-Addresse für den angegebenen Hostnamen (Abbildung 2).

Abbildung 1: Das "Seen"-Modul des Bots merkt sich, wer kommt und geht und gibt auf Anfrage preis, wann ein Teilnehmer zuletzt gesichtet wurde.

Abbildung 2: Das "DNS"-Modul wartet auf Nachrichten, die mit "nslookup" beginnen und löst den nachfolgenden Hostnamen in eine IP-Addresse auf.

Der nach dem Baukastensystem erweiterbare Bot nimmt von Teilnehmern sogar Befehle zum Laden neuer Plugins entgegen. Konfiguriert man den Bot mit $bot->load('Loader'), darf jeder Teilnehmer nach Belieben im Lademechanismus des Bots herumfuhrwerken. Auf die Nachricht !load DNS lädt der Bot das DNS-Modul, auf !unload DNS deaktiviert er es wieder. Das ist natürlich riskant, und Bot::BasicBot::Pluggable versucht, die Möglichkeiten mit einem Auth-Modul Nutzer einzuschränken, aber dieses Verfahren lässt sich, wie die Dokumentation zugibt, leicht aushebeln, und ist deshalb nicht empfehlenswert.

Schreiben's bitte mal mit

Ein neuer Plugin ist auch schnell erstellt. Bot-Schreiber leiten neue Plugin-Klassen einfach von der Basisklasse Bot::BasicBot::Pluggable::Module ab. Wie Listing Log.pm zeigt, sind lediglich einige Methoden zu überladen, um ein voll funktionsfähiges Plugin-Modul aus dem Boden zu stampfen. Die Methode init() ruft das Plugin-Framework einmal auf, wenn der Plugin nach dem Programmstart eingebunden wird. Da eine Referenz auf das Plugin-Objekt beiliegt, nutzt init() in Log.pm die Gelegenheit dazu, den persistenten Datenspeicher Cache::Historical zu initialisieren und eine Referenz darauf im Objekt abzuspeichern. Später, in der Methode told(), wird es diese wieder hervorholen, um ein gerade aufgeschnapptes Konversationsschnipsel dort abzulegen. Cache::Historical vom CPAN wurde eigentlich für Aktienkurse geschrieben, die es gemeinsam mit dem jeweiligen Datum in einer SQLite-Datenbank abspeichert, aber das Format eignet sich genauso gut für abzuspeichernde Chat-Nachrichten, die ebenfalls dort ebenfalls einem Schlüssel (dem Chatroom) und dem aktuellen Zeitstempel stehen. Die Variable $SQLITE_FILE in Zeile 10 von Log.pm gibt die Lage der SQLite-Datei im Filesystem an und ist eventuell in einen absoluten Pfad umzuwandeln oder anderweitig an die lokalen Gegebenheiten anzupassen.

Die Methode help() ist vorgeschrieben und liefert einen kurzen Hilfetext zurück, damit Anwender wissen, was der Plugin eigentlich treibt.

Abbildung 3: Der Snapshot-Logger lauscht in eine Konversation hinein.

Listing 2: Log.pm

    01 package 
    02      Bot::BasicBot::Pluggable::Module::Log;
    03 use warnings;
    04 use strict;
    05 use base 
    06       qw(Bot::BasicBot::Pluggable::Module);
    07 use Cache::Historical 0.03;
    08 use Log::Log4perl qw(:easy);
    09 
    10 our $SQLITE_FILE = "irclog.dat";
    11 
    12 ###########################################
    13 sub init {
    14 ###########################################
    15     my($self) = @_;
    16 
    17     $self->{logbot_cache} = 
    18         Cache::Historical->new(
    19             sqlite_file => $SQLITE_FILE,
    20         );
    21 }
    22 
    23 ###########################################
    24 sub help {
    25 ###########################################
    26     return "Logs chats in SQLite";
    27 }
    28 
    29 ###########################################
    30 sub told {
    31 ###########################################
    32     my ($self, $msg) = @_;
    33 
    34     my $val = "$msg->{who}: $msg->{body}";
    35     my $key = $msg->{channel};
    36     my $dt  = DateTime->now(
    37                      time_zone => "local");
    38 
    39     DEBUG "$dt $val";
    40 
    41     $self->{logbot_cache}->set(
    42         $dt, $key, $val );
    43 
    44     return "";
    45 }
    46 
    47 1;

Die Methode told() schließlich springt das Plugin-Framework jedes Mal an, wenn jemand im Chatroom seine Stimme erhebt. Neben einer Referenz auf das Plugin-Objekt erhält told() einen Message-Hash, der unter den Schlüsseln who und body das Kürzel des Absenders und den Inhalt der Nachricht mit sich führt. Das Modul holt anschließend mit der Method now() des DateTime-Objektes die aktuelle Uhrzeit als DateTime-Objekt ab, genau so, wie Cache::Historical den Zeitstempel erwartet. Der Parameter time_zone ist bei now() auf den Wert "local" gesetzt, damit das DateTime-Objekt in der aktuellen Zeitzone beheimatet ist. Die Methode liefert einen leeren String an das Framework zurück, um zu signalisieren, dass keine Nachricht zurück in den Chatroom geschickt wird, denn der Plugin möchte einfach stillschweigend mitprotokollieren.

Listing 3: logbot

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Bot::BasicBot::Pluggable;
    04 use Log::Log4perl qw(:easy);
    05 
    06 Log::Log4perl->easy_init({ 
    07   level  => $ERROR, 
    08   layout => "%F{1}-%L %m%n"
    09 });
    10 
    11 my $bot = Bot::BasicBot::Pluggable->new(
    12     channels => ["#perlsnapshot"],
    13     server   => "irc.freenode.net",
    14     nick     => "snapshot-logger",
    15 );
    16 
    17 $bot->load("Log");
    18 
    19   # Connect to IRC server
    20 $bot->run();

Das Skript in Listing logbot startet einen Bot, der das neue Log-Modul lädt und sich dann mit dem Channel auf dem IRC-Server verbindet. Wahlweise ist auch ein Array von Channels möglich, in die der Bot gleichzeitig eindringt. Die Methode load() in Zeile 17 findet das Modul Log.pm entweder im aktuellen Verzeichnis oder im Plugin-Verzeichnis des Framworks, das sich meist in /usr/local/lib/perl5/site_perl/5.x/Bot/BasicBot/Pluggable befindet. Der Loglevel des Skripts ist mit Log4perl auf $ERROR gesetzt, wer möchte, dass zu Debug-Zwecken auf STDERR detaillierte Meldungen über eingefangene Nachrichten und dem Speichervorgang abgesetzt werden, setzt ihn statt dessen auf $DEBUG.

Wer sehen möchte, wie die abgespeicherten Daten auf der Festplatte liegen, darf gerne einmal einen Blick hinter die Kulissen des Speichermoduls werfen: Wie Abbildung 4 zeigt, legt es ohne Zutun des Bot-Programmierers eine SQLite-Datenbank mit einem passenden Schema an und speichert jede eingehende Nachricht in einer Tabellenzeile ab. Die Einträge sind mit einer ID durchnumeriert, enthalten den Zeitstempel in der zweiten Kolumne, den Chatroom in der vierten und die Nachricht schließlich in der fünften. Die dritte Kolumne upd_time braucht hier nicht zu interessieren, die nutzt Cache::Historical für interne Zwecke. Da das Modul nur ein Feld für den zu speichernden Wert bereitstellt, presst Log.pm einfach Sender und Nachricht in einen String und separiert sie mit einem Doppelpunkt und einem Leerzeichen.

Abbildung 4: Das Modul Cache::Historical legt die gespeicherten Daten in einer SQLite-Datenbank ab.

Doch diese Darstellung sollte uns nicht interessieren, bietet Cache::Historical doch die Methode values() an, die alle unter einem Key (dem Chatroom) gespeicherten Nachrichten in chronologischer Reihenfolge zurückgibt. Listing logdump zeigt die Implementierun, Abbildung 5 die Ausgabe des Skripts.

Listing 4: logdump

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Cache::Historical 0.03;
    04 
    05 our $SQLITE_FILE = "irclog.dat";
    06 
    07 my $cache = Cache::Historical->new(
    08             sqlite_file => $SQLITE_FILE,
    09         );
    10 
    11 for my $result ( $cache->values("#perlsnapshot") ) {
    12     my($dt, $msg) = @$result;
    13 
    14     print "$dt $msg\n";
    15 }

Abbildung 5: Das Skript logdump gibt die in der SQLite-Datenbank mitgeschnittene Konversation auf der Standardausgabe aus.

Installation

Die erforderlichen Module Bot::BasicBot::Pluggable, Cache::Historical und Log::Log4perl liegen auf dem CPAN bereit und lassen sich mit einer CPAN-Shell installieren, die die Abhängigkeiten von weitern Modulen automatisch auflöst. Der zu überwachende Chat-Room und der verwendete IRC-Server ist im Skript logbot anzupassen, bevor der Bot startet. Die verwendete SQLite-Datenbank mitsamt dem verwendeten Schema legt das Modul Cache::Historical selbständig an. Außer dem Anpassen der Variablen $SQLITE_FILE an den gewünschten Pfad ist keinerlei Vorbereitung ist erforderlich. Endlich kann ich also bei Konferenzen mitschneiden, was die Leute so tuscheln, während ich einen Vortrag halte. Und das Skript muss nicht nicht mal auf dem Vortragslaptop mit wackeligem oder abgeschalteten Wifi laufen, sondern kann von einer anderen Netzwerkverbindung irgendwo auf dem Internet gestartet werden. Ich bin gespannt!

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2009/11/Perl

[2]

``Ist das nicht cool?'', Michael Schilli, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2006/03/Ist-das-nicht-cool

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.