XMPP-Kompendium: Programmierung mit Ruby
Jabber ist ein freies, offenes und standardisiertes Instant Messaging-Protokoll. Ganz einfach erweitern kann man es beispielsweise durch Bots. Diese brauchen nur die wenig komplexe Logik von Jabber-Clients zu beherrschen.
Anleitung
[Bearbeiten]Hier wird ein Bot geschrieben, der für den Benutzer über Google suchen kann. Als Programmiersprache wird Ruby verwendet, da sie schön übersichtlich und einsteigerfreundlich, aber dennoch flexibel ist.
Als Jabber-Bibliothek für Ruby wird XMPP4R verwendet. Sie ist ebenfalls relativ einfach zu bedienen, gut durchschaubar und noch besser erweiterbar. Auf Erweiterung wollen wir hier aber gar nicht eingehen, denn sie bietet uns schon alles was wir benötigen. Ganz leicht installieren kann man sie beispielsweise unter FreeBSD über die Ports: net-im/ruby-xmpp4r
Am einfachsten bekommt man XMPP4R über rubygems:
gem install xmpp4r
Jabber-Verbindung aufbauen
[Bearbeiten]Um Jabber zu sprechen, binden wir natürlich unsere gewünschte Bibliothek ein:
require 'rubygems' # <== benötigt, wenn rubygems benutzt wurde require 'xmpp4r'
Zuerst benötigen wir eine Instanz eines Jabber-Clients:
client = Jabber::Client.new(Jabber::JID.new('bot@server.com/google'))
Hier haben wir schon die gewünschte Jabber-ID (JID) unseres Agenten angegeben: es ist Benutzer bot, registriert auf server.com und wird sich mit der Resource google anmelden. Diesen Account muss es schon auf dem Server geben. Falls Du zu faul bist einen anzulegen, leih dir bei Astro einen auf Anfrage aus!
Als nächstes muss sich der Client zum Server verbinden:
client.connect
Das praktische an Jabber ist, dass Clients nur eine TCP-Verbindung offenhalten müssen. Die restliche Logik wird vom Server gehandhabt.
Jetzt noch authentifizieren:
client.auth('passwort')
Es soll ja nicht jeder unseren Bot-Account verwenden können. Zur Passwortübertragung noch ein Wort der Beruhigung: es wird lediglich der SHA1-Hash übertragen. Den könnte allerdings jeder übertragen, deshalb holt der Client sich beim Verbindungsaufbau noch eine Session-ID. Diese wird an das Passwort angehangen, dann wird eigentlich der Hash von Passwort und Session-ID übertragen. Ziemlich sicher, solange keiner den Server hijackt oder den Speicher des Clientprogrammes auslesen kann.
Online-Status übertragen
[Bearbeiten]Zunächst sollten wir noch bekannt machen, dass unser Bot online ist. Das macht der Server nicht per default, vielleicht wollen wir auch gar nicht länger online bleiben, sondern nur eben eine Nachricht versenden.
Hier aber nicht, hier wollen wir, dass alle wissen dass unser Agent da ist.
client.send() (korrekt: Jabber::Client#send) ist die Methode mit der wir alles rausschicken können. Hier also unser Online-Status, genannt Presence:
client.send(Jabber::Presence.new)
Wir wollen aber noch mehr. Wir wollen Free for chat sein und eine hübsche Status-Message anzeigen. Um nicht erst eine Instanz von Presence holen zu müssen, die wir erst ändern und dann abschicken, hat XMPP4R etwas ganz innovatives eingeführt: Chaining. Das sind Setter, die als Rückgabewert das Objekt selbst haben. Wir können diese also hintereinanderketten:
client.send(Jabber::Presence.new.set_show(:chat).set_status('I will google for you!'))
Andererseits nimmt diese Parameter auch schon der Constructor von Presence entgegen, man könnte es also auch so schreiben:
client.send(Jabber::Presence.new(:chat, 'I will google for you!'))
XMPP-Stanzas debuggen
[Bearbeiten]XMPP (das Jabber-Protokoll) besteht aus drei Hauptelementen, den sogenannten Stanzas:
- <message/> für Nachrichten
- <presence/> für Online-Status
- <iq/> für alle anderen Abfragen, zum Beispiel vCards (User-Info), Client-Versionen und vieles mehr
In XMPP4R werden diese durch die Klassen Message, Presence und Iq repräsentiert, welche von REXML::Element abgeleitet sind, also alle Eigenschaften von XML-Elementen haben.
Beispielsweise können wir diese nun ganz einfach in Interactive Ruby anschauen:
% irb irb(main):001:0> require 'xmpp4r' => true irb(main):002:0> Jabber::Presence.new(:chat, 'I will google for you!').to_s => "<presence><show>chat</show><status>I will google for you!</status></presence>"
Bot laufen lassen
[Bearbeiten]Letztendlich dürfen wir das Skript nicht einfach beenden lassen, sondern müssen in eine Art Hauptschleife eintreten. Das ist bei XMPP4R hier jedoch gar nicht nötig, da wir den Client schon implizit im Threaded mode gestartet haben. Es läuft also schon längst ein Thread des Clients, der sich um alles kümmert.
Wir müssen also nur noch den Hauptthread anhalten:
Thread.stop
Jetzt darf schon getestet werden. Der Bot geht mit gewünschtem Status online und macht nichts. Schön.
Suchmaschine implementieren
[Bearbeiten]Jetzt kommen wir zur Funktionalität unseres Agenten: googlen soll er!
Dazu bauen wir erst einmal eine neue Unterfunktion:
def google(phrase) end
Als nächstes müssen wir Google unseren Suchwunsch übergeben. Natürlich escaped, wer weiss mit was die User den armen, kleinen Bot füttern. Das kann folgende Funktion für uns übernehmen:
CGI::escape(phrase)
Dafür benötigen wir die cgi-Bibliothek, also an den Anfang des Skripts:
require 'cgi'
Die komplette URL lautet nun: "http://www.google.com/search?q=#{CGI::escape(phrase)}". Das #{...} können wir machen, weil der String in Anführungszeichen statt Hochkommata steht. Da kommt dann einfach gewünschter Code, eben unser escaped Suchwort rein.
Natürlich müssen wir das noch in eine HTTP-Anfrage umformulieren:
response = Net::HTTP::get_response('www.google.com', "/search?q=#{CGI::escape(phrase)}")
Für Net::HTTP brauchen wir am Anfang des Skripts noch:
require 'net/http'
Jetzt haben wir eine response. Unser Suchergebnis befindet sich in response.body. Leider hat uns Google ein Ergebnis mit dem Zeichensatz ISO-8859-1 geliefert. Jabber ist jedoch glücklicherweise UTF-8. Schicken wir ihm ungültige Zeichen, dann wird uns der Server sofort trennen. Deshalb müssen wir erstmal mithilfe der iconv-Bibliothek konvertieren:
html = Iconv.new('utf-8', 'iso-8859-1').iconv(response.body)
Dafür brauchen wir am Anfang:
require 'iconv'
Das Paket gibt es unter FreeBSD im Port converters/ruby-iconv.
Jetzt sind wir bereit, unser zeichensatzkonvertiertes Suchergebnis auseinanderzupfriemeln.
Zuerst initialisieren wir unser Ergebnis-Array:
result = []
Jetzt wandern wir mit einem regulären Ausdruck durch das Suchergebnis, um uns URLs und Seitentitel herauszuklauben:
html.body.scan(/<a class=l href="(.+?)">(.+?)<\/a>/) { |url,title|
In diesem Block hängen wir einen Ergebnisstring an unser Ergebnis-Array an:
result.push("#{title}: #{url}") }
In Ruby liefern Funktionen immer das Ergebnis der letzten Operation zurück. Also operieren wir einfach nichts mit unserem result und so sieht das Ende unserer google-Funktion aus:
result end
Nach diesen sieben Zeilen Funktionsrumpf haben wir schon die Googlesuche implementiert:
def google(phrase) response = Net::HTTP::get_response('www.google.com', "/search?q=#{CGI::escape(phrase)}") result = [] html = Iconv.new('utf-8', 'iso-8859-1').iconv(response.body) html.scan(/<a class=l href="(.+?)">(.+?)<\/a>/) { |url,title| result.push("#{title}: #{url}") } result end
Jetzt müssen wir natürlich noch die Verbindung zu Jabber herstellen...
Message-Callback implementieren
[Bearbeiten]Irgendwo zwischen Client-Instantiierung und Stoppen des Hauptthreads schreiben wir nun den Teil, der Suchanfragen entgegennimmt und das Ergebnis ausgibt. Dazu kann man bei XMPP4R sogenannte Callbacks schreiben, die über Namen, Prioritäten und viel mehr verfügen. Weil wir diese Komplexität aber nicht brauchen, lassen wir die Parameter weg und geben uns mit den Defaults zufrieden. Unser Block bekommt genau einen Parameter: die Nachricht.
client.add_message_callback { |msg| }
Jetzt müssen wir prüfen, ob der gesandte Text (body) nicht nil ist. Das muss sein, da zum Beispiel bei Chat State Notifications Nachrichten ohne Text verschickt werden, wenn jemand mit Tippen anfängt.
client.add_message_callback { |msg| if msg.body end }
Unter dieses if schreiben wir nun das Holen der Suchergebnisse:
searchresult = google(msg.body)
Die haben wir jetzt in einem Array aus Strings. Jetzt bauen wir uns unsere Message in einer Variable namens answer zusammen, welche an den Absender der msg geschickt wird:
answer = Jabber::Message.new(msg.from) answer.type = :chat # Alles andere nervt
Als Text unserer Nachricht möchten wir die ersten fünf Suchergebnisse, jeweils durch einen Zeilenumbruch getrennt:
answer.body = searchresult[0..4].join("\n")
Und schließlich schicken wir das über unsere Client-Verbindung ab:
client.send(answer)
Et voilà, wir haben den Joogle-Bot gebaut.
Zusammenfassung
[Bearbeiten]#!/usr/bin/env ruby require 'xmpp4r' require 'net/http' require 'cgi' require 'iconv' def google(phrase) response = Net::HTTP::get_response('www.google.com', "/search?q=#{CGI::escape(phrase)}") result = [] html = Iconv.new('utf-8', 'iso-8859-1').iconv(response.body) html.scan(/<a class=l href="(.+?)">(.+?)<\/a>/) { |url,title| result.push("#{title}: #{url}") } result end client = Jabber::Client.new(Jabber::JID.new('bot@server.com/google')) client.connect client.auth('passwort') client.send(Jabber::Presence.new.set_show(:chat).set_status('I will google for you!')) #client.send(Jabber::Presence.new(:chat, 'I will google for you!')) client.add_message_callback { |msg| if msg.body searchresult = google(msg.body) answer = Jabber::Message.new(msg.from) answer.type = :chat # Alles andere nervt answer.body = searchresult[0..4].join("\n") client.send(answer) end } Thread.stop
Quelle
[Bearbeiten]Original unter http://wiki.bsd-crew.de/index.php/Jabberbots_mit_XMPP4R
Freigegeben mit Erlaubnis des Autors.
Weiterleitung
[Bearbeiten]- Die Programmiersprache Ruby: http://www.ruby-lang.org/en/
- Das Instant-Messaging-Protokoll Jabber: http://www.jabber.org/
- Die Suchmaschine Google: http://www.google.com/
- Die Library XMPP4R: http://home.gna.org/xmpp4r/
- Dokumentation zu XMPP4R: http://home.gna.org/xmpp4r/rdoc/