Description
Jubjub is an object to XMPP mapping library designed to be used inside of a web app to simplify the administration of XMPP resources. It aims to provide a very simple interface which hides the complexity of the XMPP protocol allowing your code to communicate intent rather than implementation details. This also makes the code significantly easier to mock and test.
Currently it uses xmpp_gateway to communicate to the XMPP server. This allows Jubjub to be used within non evented web servers like Phusion Passenger.
It is currently alpha software and merely a proof of concept.
Requirements
It is currently fully tested against:
- Ruby 1.8.7
- Ruby 1.9.2
Examples
require 'jubjub'
class User
attr_accessor :xmpp_jid, :xmpp_password
# :jid and :password are used to point to methods that contain
# the credentials required to login as a user
include Jubjub::User
jubjub_client :jid => :xmpp_jid, :password => :xmpp_password
end
u = User.new
u.xmpp_jid = "theozaurus@jabber.org"
u.xmpp_password = "ruby"
# Sensibly defaults to conference.YOUR_CONNECTION_JID
u.mucs
=> [
#<Jubjub::Muc:0x1027222b8 @jid="all@conference.jabber.org" @name="all">,
#<Jubjub::Muc:0x102722038 @jid="all-linux-ru@conference.jabber.org" @name="all-linux-ru">,
#<Jubjub::Muc:0x102721b60 @jid="allgorithmus@conference.jabber.org" @name="Allgorithmus Project">,
...
# Fancy a list from a different service?
u.mucs('conference.jabber.ru')
=> [
#<Jubjub::Muc:0x10166e930 @jid="tota-room@conference.jabber.ru" @name="tota-room (5)">,
#<Jubjub::Muc:0x10166e6b0 @jid="friends_room@conference.jabber.ru" @name="Friends (6)">,
#<Jubjub::Muc:0x10166dcb0 @jid="think_linux@conference.jabber.ru" @name="think_linux (n/a)">
...
# Find a room
u.mucs('conference.jabber.ru')['tota-room']
=> #<Jubjub::Muc:0x10166e930 @jid="tota-room@conference.jabber.ru" @name="tota-room (5)">
# Create a room
room = u.mucs.create('jubjub')
=> #<Jubjub::Muc:0x101532f58 @jid="jubjub@conference.jabber.org" @name=nil>
# Create a room somewhere else
room = u.mucs('chat.test.com').create('jubjub')
=> #<Jubjub::Muc:0x10161c3b0 @jid="jubjub@chat.test.com" @name=nil>
# Create a room with a custom configuration
room = u.mucs.create('customjub'){|c|
c['muc#roomconfig_allowinvites'] = false
}
# Retrieve current affiliations
room.affiliations
=> [#<Jubjub::Muc::Affiliation:0x1019a3c90 @muc_jid="jubjub@chat.test.com" @jid="theozaurus@jabber.org" @nick=nil @affiliation="owner" @role=nil>]
# Search affiliations
room.affiliations['theozaurus@jabber.org']
=> #<Jubjub::Muc::Affiliation:0x1019a3c90 @muc_jid="jubjub@chat.test.com" @jid="theozaurus@jabber.org" @nick=nil @affiliation="owner" @role=nil>
# Test affiliations
room.affiliations['theozaurus@jabber.org'].owner?
=> true
room.affiliations['theozaurus@jabber.org'].member?
=> false
# Create affiliation
room.affiliations['bob@test.com'].set_admin
=> true
# or
room.affiliations['bot@test.com'].set('admin')
=> true
# or
room.add_affiliations {"bot@test.com" => "admin", "theozaurus@jabber.org" => "owner"}
=> true
# Message a room
room.message "I am an invisible man."
=> #<Jubjub::Muc:0x10161c3b0 @jid="customjub@chat.test.com" @name=nil>
# Destroy a room
room.destroy
=> true
# Create a persistent room and exit it
u.mucs.create('persistent'){|c|
c['muc#roomconfig_persistentroom'] = true
}.exit
# List pubsub nodes
u.pubsub
=> [
#<Jubjub::Pubsub:0x101f23c88 @service="pubsub.jabber.org" @node="facts">,
#<Jubjub::Pubsub:0x101f23a30 @service="pubsub.jabber.org" @node="news">
]
# Find a pubsub node
u.pubsub['facts']
=> #<Jubjub::Pubsub:0x101f23c88 @service="pubsub.jabber.org" @node="facts">
# List from a different service?
u.pubsub('pubsub.jabber.ru')
=> [
...
]
# Create a pubsub node
node = u.pubsub.create('node_1')
=> #<Jubjub::Pubsub:0x101f3bae0 @service="pubsub.jabber.org" @node="node_1">
# Create a pubsub node with a custom configuration
u.pubsub.create('node_2'){|c|
c['pubsub#title'] = "Node 2"
}
# Subscribe to node
subscription = node.subscribe
=> #<Jubjub::Pubsub::Subscription:0x101effd60 @service="pubsub.jabber.org" @node="node_1" @subscriber="theozaurus@jabber.org" @subid="5129CD7935528" @subscription="subscribed">
# Subscribe to something else
u.pubsub.subscribe('new_thing')
=> #<Jubjub::Pubsub::Subscription:0x101effd60 @service="pubsub.jabber.org" @node="new_thing" @subscriber="theozaurus@jabber.org" @subid="5129CD7935528" @subscription="subscribed">
# Subscribe someone else to the node
u.pubsub["new_thing"].subscriptions["foo@jabber.org"].subscribe
=> true
# Subscribe multiple jids
u.pubsub["new_thing"].add_subscriptions {"foo@jabber.org" => "subscribed", "bar@jabber.org" => "subscribed"}
=> true
# Show subscriptions
u.pubsub["new_thing"].subscriptions
=> [#<Jubjub::Pubsub::Subscription:0x7f99fda76c48 @jid="pubsub.jabber.org" @node="new_thing" @subscriber="theozaurus@jabber.org" @subid="5129CD7935528" @subscription="subscribed">, #<Jubjub::Pubsub::Subscription:0x7f99fda76770 @jid="pubsub.jabber.org" @node="new_thing" @subscriber="foo@jabber.org" @subid="53876943C09A7" @subscription="subscribed">]
# Publish to a node
item = u.pubsub['node_1'].publish(Jubjub::DataForm.new({:foo => {:type => 'boolean', :value => 'bar'}}))
=> #<Jubjub::Pubsub::Item:0x101f2e9f8 @jid="pubsub.jabber.org" @node="node_1" @item_id="519DCAA72FFD6" @data="<x xmlns=\"jabber:x:data\" type=\"submit\">\n <field var=\"foo\">\n <value>false</value>\n </field>\n</x>">
# Retract an item from a node
item.retract
=> true
# Or
u.pubsub['node_1'].retract('abc')
=> true
# List items on a node
u.pubsub['node_1'].items
=> [
#<Jubjub::Pubsub::Item:0x101f7bd48 @jid="pubsub.jabber.org" @node="node_1" @item_id="519DCAA72FFD6" @data="...">,
...
]
# Retrieve an item from a node
u.pubsub['node_1'].items['519DCAA72FFD6']
=> #<Jubjub::Pubsub::Item:0x101f7bd48 @jid="pubsub.jabber.org" @node="node_1" @item_id="519DCAA72FFD6" @data="...">
# Retrieve affiliations from a node
u.pubsub['node_1'].affiliations
=> [
#<Jubjub::Pubsub::Affiliation:0x101f52830 @pubsub_jid="pubsub.jabber.org" @pubsub_node="node_1" @jid="theozaurus@jabber.org" @affiliation="owner">
...
]
# Search affiliations
u.pubsub['node_1'].affiliations['theozaurus@jabber.org']
=> #<Jubjub::Pubsub::Affiliation:0x101f52830 @pubsub_jid="pubsub.jabber.org" @pubsub_node="node_1" @jid="theozaurus@jabber.org" @affiliation="owner">
# Test affiliations
u.pubsub['node_1'].affiliations['theozaurus@jabber.org'].owner?
=> true
u.pubsub['node_1'].affiliations['theozaurus@jabber.org'].publisher?
=> false
# Set affiliation
u.pubsub['node_1'].affiliations['theozaurus@jabber.org'].set_publisher
=> true
# or
u.pubsub['node_1'].affiliations['theozaurus@jabber.org'].set('publisher')
=> true
# or
u.pubsub['node_1'].add_affiliations {'theozaurus@jabber.org' => 'publisher, "foo@bar.com" => "owner"}
=> true
# Purge a node
node.purge
=> true
# Or
u.pubsub.purge('node_1')
=> true
# Unsubscribe
subscription.unsubscribe
=> true
# Destroy a node directly
node.destroy
=> true
# Destroy another node
u.pubsub.destroy('spare_node')
=> true
# Destroy another node with redirect
u.pubsub.destroy('old_node', 'pubsub.jabber.ru', 'new_new')
=> true
Dealing with errors
Every returned object from every action is really a Jubjub::Response::Proxy, which delegates to the Jubjub::Response and if the method doesn't exist there to the actual result. As Jubjub::Response only has a few methods it will usually just behaves like the actual result (say a Jubjub::Pubsub::Item). However, you can check what has really happened like so:
# Attempt to destroy a node that does not exist
u.pubsub.destroy('made up')
=> false
# False tells us destroy didn't succeed in this case, but it's not terribly helpful
# So we can use methods provided by the Jubjub::Response as well
response = u.pubsub.destroy('made up')
response.success?
=> false
response.stanza.to_s
=> "<?xml version=\"1.0\"?>\n<iq type=\"error\" id=\"blather0011\" from=\"pubsub.theo-template.local\" to=\"theozaurus@theo-template.local/42607076141308656900975361\" lang=\"en\">\n <pubsub xmlns=\"http://jabber.org/protocol/pubsub#owner\">\n <delete node=\"made up\"/>\n </pubsub>\n<error code=\"404\" type=\"cancel\"><item-not-found xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/></error></iq>\n"
# We can get the real result with no proxy sat in front
response.result
=> false
response.result.success?
NoMethodError: undefined method `success?' for false:FalseClass
from (irb):14
# We can also get an enhanced error object, Jubjub::Response::Error
response.error
=> #<Jubjub::Response::Error:0x101ef51f8 @stanza=#<Nokogiri::XML::Element:0x80f7a7bc name="error" attributes=[#<Nokogiri::XML::Attr:0x80f7a654 name="code" value="404">, #<Nokogiri::XML::Attr:0x80f7a640 name="type" value="cancel">] children=[#<Nokogiri::XML::Element:0x80f7a03c name="item-not-found" namespace=#<Nokogiri::XML::Namespace:0x80f79fb0 href="urn:ietf:params:xml:ns:xmpp-stanzas">>]>>
response.error.stanza.to_s
=> "<error code=\"404\" type=\"cancel\">\n <item-not-found xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/>\n</error>"
response.error.condition
=> 'item-not-found'
response.error.type
=> 'cancel'
# If there is any error text this can also be extracted
response.error.text
=> 'Item not found'
The error type
and error condition
are the important factors. The type
is defined in the RFC 6121, and helps decide whether the action should be attempted again. The condition
provides a bit more detail, and will always be part of the urn:ietf:params:xml:ns:xmpp-stanzas namespace.
TODO
- MUC user role control
- Better exception handling
- Service discovery
- Operations that are not IQ based, such as rosters and two way messaging
- Other backends (for servers that are evented)