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.
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.
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.
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. |
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.
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.
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. |
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!
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2009/11/Perl
``Ist das nicht cool?'', Michael Schilli, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2006/03/Ist-das-nicht-cool
Michael Schilliarbeitet 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. |