No commit activity in last 3 years
No release in over 3 years
Agent XMPP is a ruby XMPP bot framework inspired by MVC web frameworks.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

 Project Readme

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: 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, 1.9.2

The following Operating Systems are supported

Ubuntu 10.4, 10.10, 11.04

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, admin]

    - 
        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 'register' do
  on(:execute) do |form|
    form.add_title('Register')
    form.add_instructions('Enter you JID.')
    form.add_jid_single('contact_1', 'JID')
  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_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.