AgentXMPP is an application framework for writing XMPP clients that support Messaging, Ad-Hoc Commands and Publish Subscribe Events. An application that responds to an Ad-Hoc Command can be written with few lines of code.

          # myapp.rb
          require 'rubygems'
          require 'agent_xmpp'

          command 'hello' do
            'Hello World' 
          end
      

Specify the application Jabber ID (JID), password and contact roster in agent_xmpp.yml.

          jid: myapp@nowhere.com
          password: none
          roster:
              -   jid:you@home.com
                  groups: [admin]
      

Be sure libxml2 headers are available and that libsqlite3-ruby1.9.1 is installed,

          sudo apt-get install libxml2-dev
          sudo apt-get install libsqlite3-ruby1.9.1
      

Install the gem,

          sudo gem install agent_xmpp
      

Install the Gajim XMPP Client version 0.12.3 or higher, www.gajim.org, and connect to you@home.com.

Run the application,

          ruby myapp.rb
      

When started for the first time myapp.rb will automatically send contact requests to all contacts specified in the agent_xmpp.yml contact roster. If you accept the contact request myapp will appear in the Gajim contact roster. Right click on myapp and select execute commands from the drop down menu. A list of Ad-Hoc Commands will be displayed containing hello. Select it and click the forward button to execute.

See github.com/troystribling/agent_xmpp/blob/master/test/app/app.rb for many examples.

Supported Environment

The following versions of ruby are supported

          ruby 1.9.1    
      

The following Operating Systems are supported

    
          Ubuntu 10.4    
      

Contact Groups

Contact groups may be specified in agent_xmpp.yml.

          jid: myapp@nowhere.com
          password: none
          roster:
              -  
                  jid:you@home.com
                  groups: [good group, owners]
              -
                  jid: someone@somewhere.com
                  groups: [bad group]    
      

Agent Administrator Commands

Any contact that is in the admin contact group can execute Administrator Commands. At least one administrator should be specified in agent_xmpp.yml. The following commands are available to agent administrators.

  • contacts: List the contact roster.

  • online users: List all online users.

  • add contact: Add a contact.

  • delete contact: Delete a contact.

  • subscriptions: List all subscriptions with statistics.

  • publications: List all publications with statistics.

  • messages by type: List message statistics by message type.

  • messages by contact: List message statistics by contact.

  • messages by command: List message statistics by command.

Ad-Hoc Command Response Payload

Ad-Hoc Commands allow XMPP clients to send and receive structured parameterized commands. To process an Ad-Hoc Command request in an AgentXMPP application use command blocks. AgentXMPP will map native ruby scalars, arrays and hashes returned by command blocks to jabber:x:data command response payloads (see XEP-0004 xmpp.org/extensions/xep-0004.html for a description of jabber:x:data).

          command 'scalar' do
            'scalar' 
          end

          command 'hash' do
            {:a1 => 'v1', :a2 => 'v2'}
          end

          command 'scalar_array' do
            ['v1', 'v2','v3', 'v4'] 
          end

          command 'hash_array' do
            {:a1 => ['v11', 'v11'], :a2 => 'v12'}
          end

          command 'array_hash' do
            [{:a1 => 'v11', :a2 => 'v12'}, 
             {:a1 => 'v21', :a2 => 'v22'}, 
             {:a1 => 'v31', :a2 => 'v32'}]
          end

          command 'array_hash_array' do
            [{:a1 => ['v11', 'v11'], :a2 => 'v12'}, 
             {:a1 => ['v21', 'v21'], :a2 => 'v22'}, 
             {:a1 => ['v31', 'v31'], :a2 => 'v32'}]
          end
      

Ad-Hoc Command Data Forms

XMPP provides a simple form specification for entry of Ad-Hoc Command parameters, xmpp.org/extensions/xep-0004.html#protocol-fieldtypes. AgentXMPP supports the following form controls.

  • title: The form title.

  • instructions: Form usage instructions for the user.

  • fixed: Static text.

  • text-single: Single line text entry.

  • text-private: Single line private text entry for passwords.

  • jid-single: Single JID entry with syntax validation.

  • text-multi: Muli-line text entry.

  • list-single: Select a single item from a list items.

  • boolean: Select a boolean value for an item.

Form controls are specified in an on bloc which takes the command action as an argument and yields the form. Valid values for the action are :execute and :submit. In a simple form the controls are specified in on(:execute) and the response in on(:submit).

          command 'long_form' do
            on(:execute) do |form|
              form.add_title('The Long Form')
              form.add_instructions(
                'Make the correct choices and provide the required information.')
              form.add_fixed("Your name is required.")
              form.add_text_single('first_name', 'First Name')
              form.add_text_single('last_name', 'Last Name')
              form.add_fixed("Your address is required.")
              form.add_text_single('street', 'Street')
              form.add_text_single('city', 'City')
              form.add_text_single('state', 'State')
              form.add_text_single('zip', 'Zip Code')
              form.add_fixed("Enter two friends.")
              form.add_jid_single('contact_1', 'contact JID')
              form.add_jid_single('contact_2', 'contact JID')
              form.add_fixed("Your password is required.")
              form.add_text_private('password', 'Password')
              form.add_text_private('renter_password', 'Renter Password')
              form.add_fixed("Choose your food.")
              form.add_list_single('fruits', 
                [:apple, :orange, :lemon, :lime, :kiwi_fruit], 'Select a Fruit')
              form.add_list_single('nuts', 
                [:peanut, :almond, :cashew, :pecan, :walnut], 'Select a Nut')
              form.add_list_single('vegetables', 
                [:broccoli, :carrot, :corn, :tomato, :onion], 'Select a Vegtable')
              form.add_fixed("Answer the questions.")
              form.add_boolean('yes_or_no', 'Yes or No please?')
              form.add_boolean('flux_capcitors', 
                'Enable flux capacitors for superluminal transport')
              form.add_fixed("A story of at least 250 characters is required")
              form.add_text_multi('story', 'Your Story')
            end
            on(:submit) do
              params[:data]
            end
          end    
      

If command parameters have dependencies multi-step forms can be used. Multi-step forms are specified by a sequence of on(:submit) blocks that are called in the order listed.

          command 'multiple_steps' do
            on(:execute) do |form|
              form.add_title('Account Features')
              form.add_instructions('Enter and Account')
              form.add_jid_single('jid', 'account JID')
            end
            on(:submit) do |form|
              form.add_title("Account '#{params[:data]['jid']}'")
              form.add_instructions('Enable/Disbale features')
              form.add_boolean('idle_logout', 'On or Off please')
              form.add_boolean('electrocution', 'Electrocute on login failure?')
              form.add_text_multi('mod', 'Message of the day')
              form.add_text_multi('warn', 'Warning message')
            end
            on(:submit) do
              params_list.inject({}){|r,p| r.merge(p[:data])} 
            end
          end
      

Command Authorization

AgentXMPP allows command authorization groups to be specified by XMPP contact groups.

          command 'do_something', :access => 'good' do
              Something.do_it(params[:data])
          end

          command 'do_something', :access => ['bad', 'good'] do
              SomethingElse.do_it(params[:data])
          end
      

Command Before Filters

AgentXMPP supports specification of filters executed before command execution that must return a boolean value. If the filter returns true the command executes. If false is returned the command does not execute.

          before :command => :all do
            jid = params[:from]
            AgentXmpp::Roster.find_by_jid(jid) or AgentXmpp.is_account_jid?(jid)
          end

          before :command => 'do_something' do
              Something.do_it?(params)
          end

          before :command => ['do_something', 'and_something_else'] do
              Something.do_it?(params)
          end
      

Deferred Command Execution

By default AgentXMPP executes commands in the main event loop. If a command requires a lot of time for execution it can be deferred to a thread pool.

          command 'starship_engine_configuration', :defer => true do
            on(:execute) do |form|
              form.add_title('Hyper Drive Configuration')
              form.add_instructions(
                'Choose the hyperdrive configuration which best suits your needs')
              form.add_boolean('answer', 'On or Off please')
              form.add_boolean('flux_capcitors', 
                'Enable flux capacitors for superluminal transport')
              form.add_fixed('Enable SQUIDs for enhanced quantum decoherence')
              form.add_boolean('squids')
            end
            on(:submit) do
              StarshipEngineering.engage(params[:data])
            end
          end
      

Send Commands

Commands may be sent with or without a response callback,

          send_command(:to=>'thatapp@aplace.com/ahost', :node=> 'hello') do |status, data|
            puts "COMMAND RESPONSE: #{status}, #{data.inspect}"
          end

          send_command(:to=>'thatapp@a-place.com/ahost', :node=> 'bye')
      

and within command blocks.

          command 'hash_hello' do
            send_command(:to=>params[:from], :node=> 'hello') do |status, data|
              puts "COMMAND RESPONSE: #{status}, #{data.inspect}"
            end
            {:a1 => 'v1', :a2 => 'v2'}
          end
      

Command Error Response

Error responses to Ad-Hoc Command requests can be sent if an error is encountered during command execution.

    
          command 'do_something' do
            if MyValidator.can_do_something?(params)
              'I did it'
            else
              error(:bad_request, params, 'jid not specified')
            end
          end    
      

In general the error response syntax has the form,

          error(error_type, params, error_message)
      

Valid error_types are,

          :bad-request
          :conflict
          :feature-not-implemented
          :forbidden
          :gone
          :internal-server-error
          :item-not-found
          :jid-malformed
          :not-acceptable
          :not-allowed
          :not-authorized
          :payment-required
          :recipient-unavailable
          :redirect
          :registration-required
          :remote-server-not-found
          :remote-server-timeout
          :resource-constraint
          :service-unavailable
          :subscription-required
          :undefined-condition
          :unexpected-request
      

Command Response Delegation

Command responses may be delegated to one or more Message Processing Callbacks (see the last section Message Processing Callbacks for a list). Message Processing Callbacks give applications the ability to interface with the framework message processing workflow. Command Response Delegation is useful when a command must send another message and the response of this secondary message is processed by the framework. The command then delegates its response to the secondary message response. In the example below of the add_contact administration message the command sends a command to the server to add a roster item and does not respond to the original request until the response of the add roster item request is received from the server.

          command 'admin/add_contact', :access => 'admin' do
            on(:execute) do |form|
              form.add_title('Add Contact')
              form.add_jid_single('jid', 'contact JID')
              form.add_text_single('groups', 'groups comma seperated')
            end
            on(:submit) do
              contact = params[:data]
              if contact["jid"]
                AgentXmpp::Contact.update(contact)
                xmpp_msg(AgentXmpp::Xmpp::IqRoster.update(
                  pipe, contact["jid"], contact["groups"].split(/,/))) 
                xmpp_msg(AgentXmpp::Xmpp::Presence.subscribe(contact["jid"]))
                delegate_to(
                  :on_update_roster_item_result => lambda do |pipe, item_jid|     
                    command_completed if item_jid.eql?(contact["jid"])
                  end,
                  :on_update_roster_item_error  => lambda do |pipe, item_jid|
                    error(:bad_request, params, 'roster updated failed') \
                      if item_jid.eql?(contact["jid"])
                  end
                )
              else
                error(:bad_request, params, 'jid not specified')
              end
            end
          end
      

Publish

Publish nodes are configured in agent_xmpp.yml.

          jid: myapp@nowhere.com
          password: none
          roster:
              -
                jid:you@home.com
          publish:
              -              
                  node: time
                  title: "Curent Time"   
              -              
                  node: alarm
                  title: "Alarms"   
      

The nodes are created if they do not exist and publish methods are generated for each node.

          publish_time('The time is:' + Time.now.to_s)
    
          publish_alarm({:severity => :major, :description => "A really bad failure"})
      

Publish nodes discovered that are not in agent_xmpp.yml will be deleted.

Publish Options

The following publish options are available with the indicated default values. The options may be changed in agent_xmpp.yml.

          :title                    => 'event',
          :access_model             => 'presence',
          :max_items                => 20,
          :deliver_notifications    => 1,
          :deliver_payloads         => 1,
          :persist_items            => 1,
          :subscribe                => 1,
          :notify_config            => 0,
          :notify_delete            => 0,
          :notify_retract           => 0,
      

See xmpp.org/extensions/xep-0060.html#registrar-formtypes-config for a detailed description.

Subscribe

Declare event blocks in myapp.rb to subscribe to published events.

          # myapp.rb
          require 'rubygems'
          require 'agent_xmpp'

          event 'someone@somewhere.com', 'time' do
            message(:to=>'someone@somewhere.com', :body=>"Got the event at: " + Time.now.to_s)
          end
      

AgentXMPP will verify subscription to the event and subscribe if required. Subscriptions discovered that are not declared by an event block will be deleted.

Receive Chat Messages

Declare chat blocks in myapp.rb to receive and respond to chat messages.

          # myapp.rb
          require 'rubygems'
          require 'agent_xmpp'

          chat do
            params[:body].reverse
          end
      

If the chat block returns a String a response will be sent to the message sender.

Send Chat Messages

          send_chat(:to=>'thatapp@a-place.com/onahost', 
            :body=>"Hello from #{AgentXmpp.jid.to_s} at " + Time.now.to_s)    
      

Routing Priority

The routing priority may be configured in agent_xmpp.yml. The default value is 1. Valid values are between -127 and 128. See xmpp.org/rfcs/rfc3921.html for a details.

          jid: myapp@nowhere.com
          password: none
          priority: 128
          roster:
              -              
                  jid:you@home.com
                  groups: [good group, owners]        
      

Message Processing Context Extension

You can add methods to the command and chat context by adding your methods to a module and calling,

    
          include_module MyExtensions            
      

Major Event Callbacks

AgentXMPP provides callbacks for applications to respond to major events that occur during execution.

          # application starting
          before_start{}

          # connected to server
          after_connected{|connection|}

          # client restarts when disconnected form server
          restarting_client{|connection|}
    
          # a pubsub node was discovered at service
          discovered_pubsub_node{|service, node|}
    
          # command nodes were discovered at jid
          discovered_command_nodes{|jid, nodes|}

          # a presence message of status :available or :unavailable was received from jid
          received_presence{|from, status|}    
      

Authentication

  • Basic SASL

Development with XMPP Clients

Ad-Hoc Commands, jabber:x:data Forms nor Service Discovery are widely supported by XMPP clients and I have not found a client that adequately supports Publish-Subscribe. Gajim www.gajim.org provides support for Ad-Hoc Commands and jabber:x:data Forms. Service Discovery, which is useful for Publish-Subscibe development, is supported by Gajim, but Psi psi-im.org provides a much better implementation. Both Gajim and Psi provide an interface for manual entry of XML messages. Since Publish-Subscribe is not supported on the user interface manual entry of messages is required for development. Example messages can be found at gist.github.com/160344

Logging

By default log messages are written to STDOUT. A log file can be specified with the -l option.

              ruby mybot.rb -l file.log
      

The logger can be accessed and configured.

        
              before_start do
                AgentXmpp.logger.level = Logger::WARN 
              end
      

More Examples

More examples can be found at gist.github.com/160338

Supported XEPs

Message Processing Callbacks

Message Processing Callbacks are available to applications to extend the agent message processing work flow. To receive callbacks a delegate object must be provided that implements the callbacks of interest.

          after_connected do |connection|
            connection.add_delegate(YourDelegate)
          end
      

Connection

          on_connect(connection)

          on_disconnect(connection)

          on_did_not_connect(connection)
      

Authentication

          on_bind(connection)

          on_preauthenticate_features(connection)

          on_authenticate(connection)

          on_did_not_authenticate(connection)

          on_postauthenticate_features(connection)

          on_start_session(connection)
      

Presence

          on_presence(connection, presence)

          on_presence_subscribe(connection, presence)

          on_presence_subscribed(connection, presence)

          on_presence_unavailable(connection, presence)

          on_presence_unsubscribed(connection, presence)
    
          on_presence_error(pipe, presence)
      

Roster

          on_roster_result(connection, stanza)

          on_roster_set(connection, stanza)

          on_roster_item(connection, roster_item)

          on_remove_roster_item(connection, roster_item)

          on_all_roster_items(connection)

          on_update_roster_item_result(connection, item_jid)

          on_update_roster_item_error(connection, item_jid)

          on_remove_roster_item(connection, item_jid)

          on_remove_roster_item_error(connection, item_jid)
      

Service Discovery

          on_version_result(connection, version)

          on_version_get(connection, request)

          on_version_error(connection, error)
    
          on_discoinfo_get(connection, request) 

          on_discoinfo_result(connection, discoinfo)
    
          on_discoinfo_error(connection, error)
    
          on_discoitems_result(connection, discoitems)
    
          on_discoitems_get(connection, request)

          on_discoitems_error(connection, result)            
      

Applications

          on_command_set(connection, stanza)

          on_message_chat(connection, stanza)

          on_message_normal(connection, stanza)

          on_pubsub_event(connection, event, to, from)
      

PubSub

          on_publish_result(connection, result, node)

          on_publish_error(connection, result, node)

          on_discovery_of_pubsub_service(connection, jid, ident)
    
          on_discovery_of_pubsub_collection(connection, jid, node)
      
          on_discovery_of_pubsub_leaf(connection, jid, node)

          on_discovery_of_user_pubsub_root(pipe, pubsub, node)

          on_pubsub_subscriptions_result(connection, result)
    
          on_pubsub_subscriptions_error(connection, result)

          on_pubsub_affiliations_result(connection, result)
    
          on_pubsub_affiliations_error(connection, result)

          on_discovery_of_user_pubsub_root(connection, result)
    
          on_create_node_result(connection, node, result)    

          on_create_node_error(connection, node, result)    

          on_delete_node_result(connection, node, result)    

          on_delete_node_error(connection, node, result)    

          on_pubsub_subscribe_result(connection, result, node) 

          on_pubsub_subscribe_error(connection, result, node) 

          on_pubsub_subscribe_error_item_not_found(connection, result, node) 

          on_pubsub_unsubscribe_result(connection, result, node) 

          on_pubsub_unsubscribe_error(connection, result, node)   
      

ERRORS

          on_unsupported_message(connection, stanza)
      

Copyright

Copyright © 2009 Troy Stribling. See LICENSE for details.