AgentXMPP

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: [email protected]
password: none
roster:
    - 
        jid:[email protected]
        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 [email protected].

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: [email protected]
password: none
roster:
    - 
        jid:[email protected]
        groups: [good group, owners]

    - 
        jid: [email protected]
        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.(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=>'[email protected]/ahost', :node=> 'hello') do |status, data|
  puts "COMMAND RESPONSE: #{status}, #{data.inspect}"
end

send_command(:to=>'[email protected]/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: [email protected]
password: none
roster:
    - 
        jid:[email protected]
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 '[email protected]', 'time' do
  message(:to=>'[email protected]', :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=>'[email protected]/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: [email protected]
password: none
priority: 128
roster:
    - 
        jid:[email protected]
        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 © 2009 Troy Stribling. See LICENSE for details.