Flüsterer im Kanal (Linux-Magazin, Januar 2011)

Ein Lausch-Bot in einem IRC-Kanal springt bei bestimmten Schlüsselwörtern an und benachrichtigt den User über Instant Messaging.

Open-Source-Projekte wie Catalyst bieten ihren Support über IRC-Kanäle an, auf denen hochkarätige Fachleute auf Nutzeranfragen warten und dann sofort mit Rat und Tat zu Hilfe eilen. Allerdings beeinträchtigt andauerndes IRC-Gedudel die Konzentration arbeitender Geistesmenschen. Bei vollem Kanal steht die Konversation selten still und oft geht es um völlig unnütze Dinge. Der heute vorgestellte Perl-Bot lauscht auf einem IRC-Kanal und benachrichtigt seinen Herrn und Meister, falls bestimmte Schlüsselwörter, wie zum Beispiel der Name desselben, fallen.

Abbildung 1: Der Bot ymbot verhält sich still, da niemand ein Schlüsselwort erwähnt hat.

Der erste Teil der Aufgabe, die Erstellung eines IRC-Bots, geht sehr einfach von der Hand, denn das schon einmal in [2] vorgestellte CPAN-Modul Bot::BasicBot stellt ein einfach erweiterbares Framework für IRC-Bots aller Art bereit. Doch wie erregt der Bot die Aufmerksamkeit seines in tiefe Gedanken versunkenen Users? Instant Messaging mit aufpoppenden Dialogfenstern bietet sich an, und der Allround-Client Pidgin bietet die gängige Protokolle wie Yahoo Messenger, Google Talk, AIM oder MSN an.

Chat über WebAPI

Vor einiger Zeit öffnete Yahoo seinen Messenger-Service über eine Web-API, auf der sich der User zunächst einloggt und dann mittels HTTP-Requests Nachrichten mit anderen Yahoo-Messenger-Nutzern austauschen kann. Das vorgestellte Bot-Script irc2ym klinkt sich in einen IRC-Kanal ein, wartet zunächst einmal still und lauscht. Erwähnt einer der Chat-Teilnehmer ein Schlüsselwort aus der Datei ~/.irc-keywords (Abbildung 4), wirft der Bot das Skript ymsend an, das sich auf der Messenger-Web-API einloggt und die aufgeschnappte Textnachricht an einen voreingestellten Messenger-Account weiterleitet. Dies alarmiert den in Gedanken versunkenen User, der seine Arbeit unterbricht, sich dann dem IRC-Kanal zuwendet und dort sein Fachwissen beisteuert, um ahnungslosen Neulingen auf die Sprünge zu helfen.

Abbildung 2: Der IRC-Teilnehmer 'hubbelquadrat' erwähnt das Schlüsselwort "cpan" und der Flüsterer benachrichtigt den User über Y!Messenger.

Abbildung 3: Der Bot hat die Nachricht an den Y!Messenger-User weiter geleitet.

Abbildung 4: Die Liste der Schlüsselwörter, auf die der Flüsterer anspringt.

Nachrichten ausschnüffeln

Listing 1 leitet eine Klasse YMBot von der Basisklasse Bot::BasicBot ab und überlädt deren Methode said(), die der Bot immer dann aufruft, wenn ein User in einem IRC-Kanal etwas zum Besten gibt. Als zweiten Parameter reicht der Bot eine Datenstruktur hinein, die unter dem Schlüssel who den Benutzernamen des Users und unter body den Text der Nachricht führt.

In diesem Callback ruft der Bot in Zeile 24 die weiter unten definierte Funktion keyword_match() auf, die den Nachrichtentext mit einer Liste vorher eingelesener Schlüsselworte aus der Datei ~/.irc2ym-keywords vergleicht. Das Skript liest deren Zeilen zu Beginn in den globalen Array @KEYWORD_LIST ein. Passt einer der im Array @KEYWORD_LIST gespeicherten regulären Ausdrücke, triggert Zeile 25 das im selben Verzeichnis liegende Skript ymsend. Dieses nimmt den Nachrichtentext auf der Kommandozeile entgegen, loggt sich auf der Web-API ein, führt einige Autorisierungsschritte nach dem OAuth-Protokoll aus und schickt schließlich den Nachrichtentext an den in Zeile 10 in der Variablen $recipient hartkodierten User.

Listing 1: irc2ym

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use local::lib;
    04 
    05 ###########################################
    06 package YMBot;
    07 ###########################################
    08 use base qw( Bot::BasicBot );
    09 use FindBin qw($Bin);
    10 
    11 my $ymsend       = "$Bin/ymsend";
    12 my($home)        = glob "~";
    13 my $KEYWORD_LIST_FILE = 
    14                   "$home/.irc2ym-keywords";
    15 my @KEYWORD_LIST = ();
    16 
    17 keyword_list_read();
    18 
    19 ###########################################
    20 sub said {
    21 ###########################################
    22   my($self, $data) = @_;
    23 
    24   if( keyword_match( $data->{body} ) ) {
    25     my $rc = system($ymsend,
    26      "$data->{who} said: '$data->{body}'");
    27     warn "$ymsend failed: $!" if $rc;
    28   }
    29 
    30   return $data;
    31 }
    32 
    33 ###########################################
    34 sub keyword_list_read {
    35 ###########################################
    36   if( !open FILE, "<$KEYWORD_LIST_FILE" ) {
    37     warn "$KEYWORD_LIST_FILE not found";
    38     return;
    39   }
    40 
    41   while(<FILE>) {
    42     chomp;
    43     s/#.*//;
    44     next if /^\s*$/;
    45     push @KEYWORD_LIST, $_;
    46   }
    47   close FILE;
    48 }
    49 
    50 ###########################################
    51 sub keyword_match {
    52 ###########################################
    53   my($said) = @_;
    54 
    55   for my $regex (@KEYWORD_LIST) {
    56     return 1 if $said =~ /$regex/i;
    57   }
    58   return 0;
    59 }
    60 
    61 ###########################################
    62 package main;
    63 ###########################################
    64 use Bot::BasicBot;
    65 
    66 my $bot = YMBot->new(
    67   server    => "irc.freenode.com",
    68   channels  => ["#ymtest"],
    69   nick      => "ymbot",
    70   name      => "Relay to Y!M",
    71   charset   => "utf-8",
    72 );
    73 
    74 $bot->run();

Die Interaktion mit der Yahoo-WebAPI erfordert vom Skript einige Bocksprünge mit dem voreingestellten Messenger-User, dessen Passwort, sowie einem auf developer.yahoo.com einzuholenden API-Key und einem "Shared Secret" für die Applikation.

OAuth-Dschungel

Mit OAuth [4] gibt ein authentifizierter User einen Token an eine Applikation weiter, die dann in seinem Namen für eine bestimmte Zeit Aktionen ausführen kann. Im Fall des Messengers berechtigt der Token die Applikation (also das Skript) dazu, eine Stunde lang Nachrichten ins IM-Netzwerk zu senden und Antworten zu empfangen. Da das Skript aber selten anspringt und sich nach dem Abschicken der Nachricht gleich wieder beendet, brächte die Speicherung des Tokens kaum Vorteile. So authentifiziert es sich auf Yahoos Login-Seite jedesmal kurzerhand neu mit Username und Passwort (Hartkodiert als $user und $password in ymsend), holt sich einen neuen Access Token ab und führt damit den Sendebefehl aus auf der Web-API aus.

In Zeile 34 loggt das Skript den User mit $user und $passwd auf der in $login_url gespeicherten URL ein, von wo Yahoo im Body der Antwort einen Request-Token zurückschickt. Diesen leitet das Skript dann zusammen mit dem API-Key und einem dazugehörigen Geheimstring $secret and die nächste URL, $auth_token_url, weiter, die daraus einen Access-Token oauth_token samt einem oauth_token_secret fabriziert. Die Antwort des Webservers kommt im Format field=value&field=value..., die das Skript in Zeile 64 einfach als query in ein URI-Objekt einspeist und die Methode query_form dann parsen lässt -- das funktioniert, da die Daten exakt wie in einem URL mit Formparametern vorliegen.

OAuth ermöglicht es dem User, sich auf der normalen Login-Seite eines Anbieters anzumelden. Die dort erstellte Kombo aus Token und Secret identifiziert die Applikation als vom User autorisiert gegenüber dem Webservice, der auf einer völlig anderen Webseite liegen kann. Das Skript ymsend leitet Token und Secret an den Messenger-Webservice unter der $session_url weiter, die eine neue Messenger-Session einläutet und den User $user im Yahoo-Messenger-Netzwerk anmeldet. Geschieht das, sehen andere IM-Nutzer den User in ihren Buddy-Listen auftauchen und das Skript schickt ab Zeile 108 mit der POST-Methode die vorher per Kommandozeile hereingereichte Nachricht an den in $recipient definierten (und hoffentlich eingeloggten) Messenger-Nutzer ab.

Dieser letzte Schritt verlangt eine Kodierung des Requests im JSON-Format nach

    { message : "die nachricht" }

was, falls der Nachrichtentext ebenfalls Anführungszeichen enthält, eine saubere Kodierung dieser Sonderzeichen erfordert. Die Funktion qquote aus dem CPAN-Modul erledigt das schnell und unkompliziert.

Listing 2: ymsend

    001 #!/usr/local/bin/perl -w
    002 use strict;
    003 use LWP::UserAgent;
    004 use Sysadm::Install qw(qquote);
    005 use URI;
    006 use JSON;
    007 
    008 my $user         = "zangzongzing";
    009 my $passwd       = "*********";
    010 my $recipient    = "mikeschi1li";
    011 
    012 my $api_key      = "******************";
    013 my $secret       = "*************";
    014 
    015 my $login_url      = "https://login.yahoo.com/WSLogin/V1/get_auth_token";
    016 my $auth_token_url = "https://api.login.yahoo.com/oauth/v2/get_token";
    017 my $session_url    = "http://developer.messenger.yahooapis.com/v1/session";
    018 my $message_url    = "http://developer.messenger.yahooapis.com/v1/message/yahoo/$recipient";
    019 
    020 my($msg) = join ' ', @ARGV;
    021 
    022 die "usage: $0 message" unless
    023   length $msg;
    024 
    025 my $ua = LWP::UserAgent->new();
    026 
    027 my $url = URI->new( $login_url );
    028 
    029 $url->query_form(
    030   login              => $user, 
    031   passwd             => $passwd, 
    032   oauth_consumer_key => $api_key );
    033 
    034 my $resp = $ua->get( $url );
    035 
    036 if( $resp->is_error() ) {
    037   die "Can't get request token: ", 
    038   $resp->message(), " ", $resp->content();
    039 }
    040 
    041 my($request_token) = 
    042  ($resp->content() =~ /RequestToken=(.*)/);
    043 
    044 $url = URI->new($auth_token_url);
    045 
    046 $url->query_form( 
    047   oauth_consumer_key => $api_key,
    048   oauth_nonce => int( rand 10000000 ),
    049   oauth_signature => "$secret&",
    050   oauth_signature_method => "PLAINTEXT",
    051   oauth_timestamp => time(),
    052   oauth_token => $request_token,
    053   oauth_version => "1.0"
    054 );
    055 
    056 $resp = $ua->get( $url );
    057 
    058 if( $resp->is_error() ) {
    059   die "Can't get access token: ", 
    060   $resp->message(), " ", $resp->content();
    061 }
    062 
    063 my $u = URI->new();
    064 $u->query( $resp->content() );
    065 my %form = $u->query_form;
    066 
    067 $session_url = URI->new( $session_url );
    068 
    069 $session_url->query_form( 
    070   oauth_consumer_key => $api_key,
    071   oauth_nonce => int( rand 10000000 ),
    072   oauth_signature => 
    073        "$secret&$form{oauth_token_secret}",
    074   oauth_signature_method => "PLAINTEXT",
    075   oauth_timestamp => time(),
    076   oauth_token => $form{oauth_token},
    077   oauth_version => "1.0"
    078 );
    079 
    080 $resp = $ua->post( $session_url, 
    081   Content_Type => 
    082          'application/json; charset=utf-8',
    083   Content => 
    084   q[ {"presenceState"   : 0, 
    085       "presenceMessage" : "I'm alive!" }] );
    086 
    087 if( $resp->is_error() ) {
    088   die "Can't get session: ", 
    089   $resp->message(), " ", $resp->content();
    090 }
    091 
    092 my $data = from_json( $resp->content() );
    093 
    094 $message_url = URI->new( $message_url );
    095 
    096 $message_url->query_form( 
    097   oauth_consumer_key => $api_key,
    098   oauth_nonce => int( rand 10000000 ),
    099   oauth_signature => 
    100        "$secret&$form{oauth_token_secret}",
    101   oauth_signature_method => "PLAINTEXT",
    102   oauth_timestamp => time(),
    103   oauth_token => $form{oauth_token},
    104   oauth_version => "1.0",
    105   sid => $data->{sessionId},
    106 );
    107 
    108 $resp = $ua->post( $message_url, 
    109   Content_Type => 
    110          'application/json; charset=utf-8',
    111   Content => 
    112   '{"message" : ' . qquote($msg) . ' }'
    113 );
    114 
    115 if( $resp->is_error() ) {
    116   die "Can't send message: ", 
    117   $resp->message(), " ", $resp->content();
    118 }

Consumer Key erzeugen

Um einen Consumer Key mit Secret für die neugeschaffene Applikation, also das Skript ymsend, zu erzeugen, klickt der API-Entwickler auf developer.yahoo.com durch "My Projects" und "New Project" (Yahoo-Account erforderlich), woraufhin die Pop-Up-Box in Abbildung 5 erscheint. Da es sich nicht um eine Webapplikation im Browser handelt, sondern um einen Desktop-Client, ist hier "Or an application using these APIs: BOSS, Contacts, Mail ..." zu wählen.

Abbildung 5: Der Entwickler fordert einen Consumer Key für eine Desktop-Client-Applikation an.

Im daraufhin erscheinenden Formular verpasst der Entwickler der Applikation ein Namenskürzel (z.B. "irc2ymessenger") und fügt als "Description" ein paar erläuternde Worte ein. Die Auswahlbox "Kind of Application" ist auf "Client/Desktop" (nicht "Web-based) zu stellen. Unter "Access Scopes" ist dann "This app requires access to private user data." zu wählen und unter dem Wust der aufklappenden Unterpunkte die Option "Read/Write" unter "Yahoo! Messenger" (Abbildung 7).

Abbildung 6: Der Entwickler fordert einen Auth-Token für eine Desktop-Client-Applikation an.

Abbildung 7: Die Applikation verlangt Schreib/Lesezugriff auf Daten des Yahoo! Messenger.

Nach dem Abnicken der Nutzungsbedingungen erscheinen dann unmittelbar die zum Basteln des neuen Messenger-Clients notwendigen Keys. Diese werden dann mittels cut-und-paste in die Strings der Zeilen 12 und 13 des Skripts ymsend eingefügt. In Zeile 9 ist weiterhin das Passwort des Messenger-Accounts (im Beispiel der User 'zangzongzing') anzugeben, der die Nachricht initiiert. Ist noch keiner vorhanden, führt yahoo.com über den Link "Sign Up" zur Account-Registrierung.

Abbildung 8: Die fertigen API-Keys zum Basteln des Y!-Messenger-Clients.

Dann ist nur noch eine Liste mit Schlüsselwörtern in ~/.irc-keywords anzulegen und der Bot irc2ym zu starten. Es kann bis zu 20 Sekunden dauern, bis er sich auf einem vielgenutzten IRC-Server in den eingestellten Kanal eingewählt hat, aber dann erscheint der Bot unter dem Namen ymbot in der Anwesenheitsliste. Gängige IRC-Clients sind irssi (im Terminalfenster) oder der Allrounder Pidgin. Fällt ein Schlüsselwort, springt ymsend an und die Nachricht sollte über das Messenger-Protokoll beim voreingestellten und zu diesem Zeitpunkt hoffentlich eingeloggten IM-Nutzer $recipient in einem Dialogfenster eintreffen. Zeit, den Newbies auszuhelfen, was macht man nicht alles!

Infos

[1]

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

[2]

Michael Schilli, "Der Protokollant" http://www.linux-magazin.de/Heft-Abo/Ausgaben/2009/11/Der-Protokollant

[2]

Yahoo! Messenger IM API http://developer.yahoo.com/messenger/guide/ch02.html

[3]

Dokumentation zum Erzeugen eines Auth-Tokens http://developer.yahoo.com/messenger/guide/chapterintrotomessengersdk.html

[4]

OAuth-Artikel auf Wikipedia: http://en.wikipedia.org/wiki/Oauth

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.