Bernhard Häussner

XMPP (Jabber) mit Ruby am Beispiel FelicianXMPPBot

15.12.2009, 21:53
FelicianXMPPBot in action

FelicianXMPPBot in action

Das Protokoll XMPP für den (Nahezu-)Echtzeitaustausch von XML-Nachrichten (meist verwendet für IM, als „Jabber“) steht in Konkurrenz und Verbindung mit einigen aktuell mehr oder weniger neuen Technologien, die mit XML und realtime zu tun haben, wie RSS/Atom pings, realtime web oder AJAX. Und ein paar Spielereien auf diesem Gebiet können sicher nicht schaden, also habe ich mit XMPP etwas herumgebastelt.

Weil dies mit PHP irgendwie seltsam wäre und David Strohmayers Text und Präsentation über XMPP mit Java noch nicht existierte, habe ich das ganze mit Ruby gebastelt, da gibt es nämlich den Gem xmpp4r, der mit vielen Beispielen kommt. So konnte ich mal ein bisschem mit Ruby herumspielen. (Zwei Fliegen mit einer Klappe usw.)

Aus den Experimenten ist ein kleiner Server geworden, der einen Jabber-Bot betreibt und über ein einfaches TCP-Protokoll erreicht werden kann, sodass aus allen TCP-Fähigen Skripten (bash mit netcat, PHP…) einfach Nachrichten an einen Jabber-Endpunkt versendet werden können.

Code und Funktionalität bestehen aus drei Teilen:

Die Klasse Netx kümmert sich um den TCP-Server und parst das einfache Protokoll: Man schickt Zeilen und beendet einzelne Nachrichten mit einem Punkt, und die Verbindung (und damit auch die aktuelle Nachricht) mit zwei Punkten. So lassen sich die TCP-Clients leicht implementieren.

Die Klasse XmppRoute bietet nur ein wenig Grundfunktion für den Jabber-Bot. Sie vereinfacht Verbinden und Senden noch weiter, zudem extrahiert sie beim Empfang einer Nachricht einen Befehlsteil.

Der Rest kümmert sich um die Verbindung der beiden Komponenten und um das Parsen von Startup-Parametern.

Aber genug der vielen Worte, jetzt folgt der Code:

jabot.rb

#!/usr/bin/ruby

# Felician 0.9 - XMPP Wrapper
# (c) 2009 by Bernhard Häussner
# Licence: GPLv2+ http://www.gnu.org/licenses/gpl-2.0.txt
#
# This jabber wrapper will recieve messages via simple tcp.
#
# Quit by sending "exit" via jabber. 
# Send "info" via jabber to get running parameters. 
# Send "feedback [msg]" via jabber to post from jabber. 
# Connect to specified port and send messages terminated by lone "." in a line to post via TCP. 
# Send a line with ".." to quit connection. 
#
# Based on xmpp4r-0.5/data/doc/xmpp4r/examples/basic
#

require 'optparse'
require 'XmppRoute'
require 'Netx'

class FelicianXMPPBot
  def start(his_JID,my_txt_JID,my_password,listen_port)
    puts "FELC: Started. Sending to #{his_JID} from #{my_txt_JID} listening to #{listen_port}."
    my_JID=JID.new(my_txt_JID)
    netx=Netx.new
    XmppRoute.connect(my_JID,my_password) do
      on_recieve do |from,cmd,param|
        if from.strip.to_s.eql?his_JID then
          case cmd.downcase
            when 'exit' then
              send(from,"-> I've got to go!")
              puts "FELC: Got shutdown signal, waiting for children to terminate..."
              netx.halt
            when 'feedback' then send(from,param)
            when 'info'then send(from,"-> Felician sending to #{his_JID} listening to #{listen_port}. ")
            else send(from,"-> I don't know: #{cmd} #{param}")
          end
        else
          puts "XMPP: Ignored a message. "
          send(from,"Hi, #{from.strip.to_s}. Sorry, could not be deliverd. Try again later! ")
        end
      end
      
      netx.on_sended do |msg|
        send(his_JID,msg.strip)
        puts "XMPP: Client message sent. "
      end
      netx.listen(listen_port)
    end
    puts "FELC: Shutting down. "
  end
end

if $0 == __FILE__
  #defaults:
  listen_port=2000
  his_JID = "bot_admin@gmx.de"
  my_txt_JID = "i_am_a_bot@jabber.ccc.de/felician"
  my_password = "top_secret"
  # cli settings
  OptionParser.new do |opts|
  opts.banner = 'Usage: ruby jabot.rb -j jid/ressource -p password -t reciever_jid -l port'
  opts.separator ''
  opts.on('-j', '--jid JID', 'sets the sender jid') { |j| my_txt_JID = j }
  opts.on('-p', '--password PASSWORD', 'sets the sender password') { |p| my_password = p }
  opts.on('-t', '--to-jid JID', 'sets the message reciever') { |t| his_JID = t }
  opts.on('-l', '--listen PORT', 'sets the server listen port') { |l| listen_port = l }
    opts.on_tail('-h', '--help', 'Show this message') {
    puts opts
    exit
  }
  opts.parse!(ARGV)
  end
  FelicianXMPPBot.new.start(his_JID,my_txt_JID,my_password,listen_port)
end

XmppRoute.rb

require 'rubygems'
require 'xmpp4r/client'
include Jabber

class XmppRoute
  def initialize(jid,pwd)
    @cmd=Proc.new { |a,b,c| puts "XMPP: No reciever. " }
    @cl = Client.new(jid)
    @cl.connect
    @cl.auth(pwd)
    @cl.send(Presence.new)
    puts "XMPP: Connected! send messages to #{jid.strip.to_s}."
    @cl.add_message_callback { |m| process m }
  end
  def close
    @cl.close
  end
  def on_recieve(&cb)
    @cmd=cb
  end
  def send(to,mes)
    ms = Message.new(to,mes) # [_CY4_L8-R_4LLY-G4T3-R_]
    ms.type = :chat
    @cl.send(ms)
  end
  private
  def process(m)
    puts "XMPP: Got msg of type #{m.type} / #{m.name} with -#{m.body}- "
    if m.type == :chat && ! m.body.nil?
      index=(m.body.index(" ") || 0)-1
      com=m.body[0..index]
      param=(index==-1?"":m.body[index+2..-1])
      puts "XMPP: Call #{com} with --#{param}-- (caused from #{m.from})"
      @cmd.call(m.from,com,param)
    end
  end
  def self.connect(jid,pwd,&block)
    bot=XmppRoute.new(jid,pwd)
    bot.instance_eval(&block)
    bot.close
  end
end

Netx.rb

require 'socket' # Get sockets from stdlib

class Netx
  def initialize; @run=true; end
  def listen(port)
    puts "NETX: Bind to Port #{port}"
    #server = TCPServer.open(port)
    #only local:
    server = TCPServer.open("127.0.0.1",port) # Socket to listen
    while @run
      # this timeout allows us to kill this lop every few seconds...
      begin
        session = Timeout::timeout(10) { server.accept }
        Thread.start(session) { |c| connection c }
      rescue TimeoutError
        #puts "NETX: Refreshing TCPServer."
      end
    end
  end
  def on_sended(&cb); @client_cb=cb; end
  def halt; @run=false; end
  private
  def connection (client)
    puts "NETX: Client conntected..."
    client.puts("-- Hello on Felician at #{Time.now.ctime}. ") # Send the helo & time to the client
    conn_open=true;
    msg_buffer="";
    while conn_open do
      l = client.gets
      if (l.strip.eql?'..') || (l.strip.eql?'.') then
        puts "NETX: Client issued a message..."
        client.puts("-- accepted. ")
        @client_cb.call(msg_buffer)
        msg_buffer="";
        conn_open=false if l.strip.eql?'..'
      else
        msg_buffer << l
      end
    end
  ensure
    client.puts "-- Closing the connection. Bye!"
    puts "NETX: Client disconnect. "
    client.close # Disconnect from the client
  end
end

Erwartet nicht zu viel von dem Code, ist schließlich mein erster Ruby-Code, abgesehen von einigen Hello-World-Sachen. Funktioniert dafür allerdings erstaunlich gut und könnte sich schon fast eigenen, um z.B. einen Systemadministrator über wichtige Aktivitäten und Warnungen aus den verschiedensten Programmen und Wartungsscripten schnell zu informieren.

Man beachte vielleicht noch das alles in dem Code, was in anderen Programmiersprachen als schlechter Stil gälte, hier eher „the ruby way“ ist ;).

Benutzung

Eine Nachricht von der Shell schicken:

09:15  burny@home:~> echo -e "Hallo Welt\n.." | netcat localhost 2000
-- Hello on Felician at Tue Dec 15 21:16:02 2009.
-- accepted.
-- Closing the connection. Bye!

Mal sehen, ob ich das Ganze noch weiter entwickle, meistens ist doch das Senden einer ganz normalen eMail einfacher.

Kurze URL http://1-co.de/b/1X. Post to twitter

Kommentare

Dave meint: (#208)
15.12.2009, 22:30

Guter Beitrag! Ich verlink das mal. :) Ergänzt sich ja wundervoll.

Mit Ruby hatte ich noch nicht so viel zutun aber sieht so aus als wäre die API von xmpp4r etwas komplexer als die von SMACK. Kann aber auch daran liegen dass ich Ruby noch nicht wirklich durchblick ^^

Bernhard H. meint: (#209)
15.12.2009, 23:56

Danke.
Sieht ganz so aus, als würde xmpp4r etwas mehr Arbeit abnehmen (VCards usw.). Ich habe zwar eben nur kurz die Dokumentation von SMACK überflogen, die einfacheren Dinge scheinen aber (so weit eben möglich) sehr gleich. Kannst dir ja die Beispiele ansehen: http://github.com/ln/xmpp4r/tree/master/data/doc/xmpp4r/examples






 
Χρόνογραφ
© 2008-2017 by Bernhard Häussner - Impressum - Login
Kurz-Link zu dieser Seite: http://1-co.de/b/1X