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