Aus Linux-Magazin 11/2009

Perl-Skript loggt Chat-Sessions

© Ahermes, Photocase.com

Trotz Konkurrenz durch andere Messenger, Twitter & Co.: IRC ist populär wie nie. Das Chat-Protokoll lässt außerdem selbst geschriebene Bots als automatische Helferlein zu. Im folgenden Beispiel protokolliert ein solcher Bot die Sitzung und archiviert sie in einer Datenbank .

Bei Konferenzen, beim Support von Open-Source-Produkten oder bei Aktionen, die eine Koordination vieler Projektmitarbeiter erfordern, ist Internet Relay Chat, kurz IRC, nach wie vor die Nummer eins unter den Instant-Message-Angeboten. Weder die mächtige Konkurrenz von Yahoo oder Microsoft noch das offene Google-Talk-Protokoll waren bislang in der Lage, den Dinosaurier der Gruppenkommunikation aufs Altenteil zu schieben.

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 zum Programmieren eines Bot tatsächlich weniger als zehn Zeilen Code notwendig sind. Das Modul Bot::BasicBot::Pluggable treibt das Konzept noch etwas weiter, indem es selbst geschriebene Bots mit Plug-ins ausstattet, die die Teilnehmer einer Chat-Session durch spezielle Nachrichten aktivieren können. Fühlt sich ein Plugin angesprochen, führt es 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 etliche nützliche, voll funktionsfähige Plugins, die der Programmierer einfach über die »load()«-Methode aktiviert. Listing 1 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();

Das Seen-Plugin merkt sich, welche Teilnehmer sich an- und abmelden, und verrät auf das Kommando »seen Username« hin, wann der angegebene Teilnehmer zuletzt online war (Abbildung 1). Das ist besonders hilfreich, falls er sich gerade nicht im Chat aufhält. Das zweite von »botstart« aktivierte Plugin, DNS, implementiert eine einfache Schnittstelle zum Unix-Kommando »nslookup«. Es antwortet auf das Kommando »nslookup Hostname« mit der vom DNS-Server zurückgelieferten IP-Adresse für den fraglichen Hostnamen (Abbildung 2).

Abbildung 1: Das Seen-Modul des Bot merkt sich, wer kommt und geht, und gibt auf Anfrage preis, wann es einen Teilnehmer zuletzt gesichtet hat.

Abbildung 1: Das Seen-Modul des Bot merkt sich, wer kommt und geht, und gibt auf Anfrage preis, wann es einen Teilnehmer zuletzt gesichtet hat.

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

Abbildung 2: Das DNS-Modul wartet auf Nachrichten, die mit »nslookup« beginnen, und löst den nachfolgenden Hostnamen in eine IP-Adresse 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 Bot herumfuhrwerken. Auf die Nachricht »!load DNS« lädt der Bot das DNS-Modul, auf »!unload DNS« deaktiviert er es wieder.

Das ist zwar riskant und Bot::BasicBot:: Pluggable versucht die Möglichkeiten mit einem »Auth«-Modul für Nutzer einzuschränken. Aber dieses Verfahren ist, wie die Dokumentation zugibt, leicht auszuhebeln, also nicht empfehlenswert.

Bitte mitschreiben

Ein neues Plugin ist schnell erstellt. Bot-Schreiber leiten neue Plugin-Klassen einfach von der Basisklasse Bot::BasicBot:: Pluggable::Module ab. Wie Listing 2 »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 das Plugin nach dem Programmstart einzubinden ist. Da eine Referenz auf das Plugin-Objekt beiliegt, nutzt »init()« in »Log.pm« die Gelegenheit, den persistenten Datenspeicher Cache::Historical zu initialisieren und eine Referenz darauf im Objekt abzuspeichern. Später, in der Methode »told()«, holt es diese Referenz wieder hervor, um ein gerade aufgeschnapptes Konversationsschnipsel dort abzulegen.

Listing 2:
»Log.pm«

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

Cache::Historical vom CPAN entstand eigentlich mit Blick auf Aktienkurse, die es gemeinsam mit dem jeweiligen Datum in einer SQLite-Datenbank speichert. Aber das Format eignet sich genauso für Chat-Nachrichten, die dort ebenfalls unter einem Schlüssel (dem Chatroom) und mit dem aktuellen Zeitstempel Platz finden. Die Variable »$SQLITE_FILE« in Zeile 11 von Listing 2 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 das Plugin eigentlich treibt.

Redner merken

Die Methode »told()« 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«-Objekts die aktuelle Uhrzeit 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 so zu signalisieren, dass sie keine Nachricht zurück in den Chatroom schickt, denn das Plugin möchte einfach stillschweigend mitprotokollieren.

Das Skript in Listing 3 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 Framework, meist in »/usr/local/lib/perl5/site_perl/5.x/Bot/BasicBot/Pluggable«. Der Loglevel des Skripts ist mit Log4perl auf »$ERROR« gesetzt, wer möchte, dass das Skript zu Debug-Zwecken auf »Stderr« detaillierte Meldungen über eingefangene Nachrichten und den Speichervorgang weitergibt, setzt ihn dagegen auf »$DEBUG«.

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();
Abbildung 3: Der Perl-Snapshot-Logger in Gestalt eines Bot lauscht hier in eine beliebige Konversation hinein.

Abbildung 3: Der Perl-Snapshot-Logger in Gestalt eines Bot lauscht hier in eine beliebige Konversation hinein.

Hinter den Kulissen

Wer sehen will, wie die abgespeicherten Daten auf der Festplatte liegen, darf gerne 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 durchnummeriert, enthalten den Zeitstempel in der zweiten Kolumne, den Chatroom in der vierten und die Nachricht schließlich in der fünften. Die dritte Spalte »upd_time« ist hier ohne Bedeutung, sie dient internen Zwecken. Da das Modul nur ein Feld pro 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 zusammen mit einem Zeitstempel ab.

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

Doch diese Darstellung sollte 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 4 zeigt die Implementierung, 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 $msgn";
15 }
Abbildung 5: Das Skript »logdump« gibt die in der SQLite-Datenbank mitgeschnittene Konversation auf Verlangen auf der Standardausgabe wieder aus.

Abbildung 5: Das Skript »logdump« gibt die in der SQLite-Datenbank mitgeschnittene Konversation auf Verlangen auf der Standardausgabe wieder 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 weiteren Modulen automatisch auflöst. Der zu überwachende Chatroom und der verwendete IRC-Server sind im Skript »logbot« anzupassen, bevor der Bot startet. Die benutzte SQLite-Datenbank mit dem verwendeten Schema legt das Modul Cache::Historical selbstständig an. Außer dem Anpassen der Variablen »$SQLITE_FILE« an den gewünschten Pfad ist keine weitere Vorbereitung erforderlich.

Endlich kann ein Perl-Mensch bei Konferenzen mitschneiden, was die Leute so tuscheln, während er einen Vortrag hält. Das Skript dazu muss nicht mal auf dem Vortragslaptop mit wackeligem oder abgeschalteten Wifi laufen, es lässt sich irgendwo im Netz starten. (jcb)

Infos

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

[2] Michael Schilli, “Ist das nicht cool?”: [https://www.linux-magazin.de/Heft-Abo/Ausgaben/2006/03/Ist-das-nicht-cool]

Der Autor


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.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben