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.
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 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]
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 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
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
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
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
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
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
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 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 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.
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.
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.
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(:to=>'thatapp@a-place.com/onahost', :body=>"Hello from #{AgentXmpp.jid.to_s} at " + Time.now.to_s)
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]
You can add methods to the command
and chat
context by adding your methods to a module and calling,
include_module MyExtensions
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|}
-
Basic SASL
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
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 can be found at gist.github.com/160338
-
XEP-0004 jabber:x:data Forms xmpp.org/extensions/xep-0004.html
-
XEP-0030 Service Discovery xmpp.org/extensions/xep-0030.html
-
XEP-0050 Ad-Hoc Commands xmpp.org/extensions/xep-0050.html
-
XEP-0060 Publish Subscribe xmpp.org/extensions/xep-0060.html
-
XEP-0092 Software Version xmpp.org/extensions/xep-0092.html
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
on_connect(connection) on_disconnect(connection) on_did_not_connect(connection)
on_bind(connection) on_preauthenticate_features(connection) on_authenticate(connection) on_postauthenticate_features(connection) on_start_session(connection)
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)
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)
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)
on_command_set(connection, stanza) on_message_chat(connection, stanza) on_message_normal(connection, stanza) on_pubsub_event(connection, event, to, from)
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)
on_unsupported_message(connection, stanza)
Copyright © 2009 Troy Stribling. See LICENSE for details.