# Jabber::Simple - An extremely easy-to-use Jabber client library.
# Copyright 2006 Blaine Cook <blaine@obvious.com>, Obvious Corp.
#
# Jabber::Simple is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Jabber::Simple is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Jabber::Simple; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
require 'rubygems'
require 'xmpp4r'
require 'xmpp4r/roster'
require 'xmpp4r/vcard'
module Jabber
class ConnectionError < StandardError #:nodoc:
end
class Contact #:nodoc:
include DRb::DRbUndumped if defined?(DRb::DRbUndumped)
def initialize(client, jid)
@jid = jid.respond_to?(:resource) ? jid : JID.new(jid)
@client = client
end
def inspect
"Jabber::Contact #{jid.to_s}"
end
def subscribed?
[:to, :both].include?(subscription)
end
def subscription
roster_item && roster_item.subscription
end
def ask_for_authorization!
subscription_request = Presence.new.set_type(:subscribe)
subscription_request.to = jid
client.send!(subscription_request)
end
def unsubscribe!
unsubscription_request = Presence.new.set_type(:unsubscribe)
unsubscription_request.to = jid
client.send!(unsubscription_request)
client.send!(unsubscription_request.set_type(:unsubscribed))
end
def jid(bare=true)
bare ? @jid.strip : @jid
end
private
def roster_item
client.roster.items[jid]
end
def client
@client
end
end
class Simple
include DRb::DRbUndumped if defined?(DRb::DRbUndumped)
# Create a new Jabber::Simple client. You will be automatically connected
# to the Jabber server and your status message will be set to the string
# passed in as the status_message argument.
#
# jabber = Jabber::Simple.new("me@example.com", "password", "Chat with me - Please!")
def initialize(jid, password, status = nil, status_message = "Available")
@jid = jid
@password = password
@disconnected = false
status(status, status_message)
start_deferred_delivery_thread
end
def inspect #:nodoc:
"Jabber::Simple #{@jid}"
end
# Send a message to jabber user jid.
#
# Valid message types are:
#
# * :normal (default): a normal message.
# * :chat: a one-to-one chat message.
# * :groupchat: a group-chat message.
# * :headline: a "headline" message.
# * :error: an error message.
#
# If the recipient is not in your contacts list, the message will be queued
# for later delivery, and the Contact will be automatically asked for
# authorization (see Jabber::Simple#add).
#
# message should be a string or a valid Jabber::Message object. In either case,
# the message recipient will be set to jid.
def deliver(jid, message, type=:chat)
contacts(jid) do |friend|
unless subscribed_to? friend
add(friend.jid)
return deliver_deferred(friend.jid, message, type)
end
if message.kind_of?(Jabber::Message)
msg = message
msg.to = friend.jid
else
msg = Message.new(friend.jid)
msg.type = type
msg.body = message
end
send!(msg)
end
end
# Set your presence, with a message.
#
# Available values for presence are:
#
# * nil: online.
# * :chat: free for chat.
# * :away: away from the computer.
# * :dnd: do not disturb.
# * :xa: extended away.
#
# It's not possible to set an offline status - to do that, disconnect! :-)
def status(presence, message)
@presence = presence
@status_message = message
stat_msg = Presence.new(@presence, @status_message)
send!(stat_msg)
end
# Ask the users specified by jids for authorization (i.e., ask them to add
# you to their contact list). If you are already in the user's contact list,
# add() will not attempt to re-request authorization. In order to force
# re-authorization, first remove() the user, then re-add them.
#
# Example usage:
#
# jabber_simple.add("friend@friendosaurus.com")
#
# Because the authorization process might take a few seconds, or might
# never happen depending on when (and if) the user accepts your
# request, results are placed in the Jabber::Simple#new_subscriptions queue.
def add(*jids)
contacts(*jids) do |friend|
next if subscribed_to? friend
friend.ask_for_authorization!
end
end
# Remove the jabber users specified by jids from the contact list.
def remove(*jids)
contacts(*jids) do |unfriend|
unfriend.unsubscribe!
end
end
# Returns true if this Jabber account is subscribed to status updates for
# the jabber user jid, false otherwise.
def subscribed_to?(jid)
contacts(jid) do |contact|
return contact.subscribed?
end
end
# If contacts is a single contact, returns a Jabber::Contact object
# representing that user; if contacts is an array, returns an array of
# Jabber::Contact objects.
#
# When called with a block, contacts will yield each Jabber::Contact object
# in turn. This is mainly used internally, but exposed as an utility
# function.
def contacts(*contacts, &block)
@contacts ||= {}
contakts = []
contacts.each do |contact|
jid = contact.to_s
unless @contacts[jid]
@contacts[jid] = contact.respond_to?(:ask_for_authorization!) ? contact : Contact.new(self, contact)
end
yield @contacts[jid] if block_given?
contakts << @contacts[jid]
end
contakts.size > 1 ? contakts : contakts.first
end
# Returns true if the Jabber client is connected to the Jabber server,
# false otherwise.
def connected?
@client ||= nil
connected = @client.respond_to?(:is_connected?) && @client.is_connected?
return connected
end
# Returns an array of messages received since the last time
# received_messages was called. Passing a block will yield each message in
# turn, allowing you to break part-way through processing (especially
# useful when your message handling code is not thread-safe (e.g.,
# ActiveRecord).
#
# e.g.:
#
# jabber.received_messages do |message|
# puts "Received message from #{message.from}: #{message.body}"
# end
def received_messages(&block)
dequeue(:received_messages, &block)
end
# Returns true if there are unprocessed received messages waiting in the
# queue, false otherwise.
def received_messages?
!queue(:received_messages).empty?
end
# Returns an array of presence updates received since the last time
# presence_updates was called. Passing a block will yield each update in
# turn, allowing you to break part-way through processing (especially
# useful when your presence handling code is not thread-safe (e.g.,
# ActiveRecord).
#
# e.g.:
#
# jabber.presence_updates do |friend, new_presence|
# puts "Received presence update from #{friend}: #{new_presence}"
# end
def presence_updates(&block)
updates = []
@presence_mutex.synchronize do
dequeue(:presence_updates) do |friend|
presence = @presence_updates[friend]
next unless presence
new_update = [friend, presence[0], presence[1]]
yield new_update if block_given?
updates << new_update
@presence_updates.delete(friend)
end
end
return updates
end
# Returns true if there are unprocessed presence updates waiting in the
# queue, false otherwise.
def presence_updates?
!queue(:presence_updates).empty?
end
# Returns an array of subscription notifications received since the last
# time new_subscriptions was called. Passing a block will yield each update
# in turn, allowing you to break part-way through processing (especially
# useful when your subscription handling code is not thread-safe (e.g.,
# ActiveRecord).
#
# e.g.:
#
# jabber.new_subscriptions do |friend, presence|
# puts "Received presence update from #{friend.to_s}: #{presence}"
# end
def new_subscriptions(&block)
dequeue(:new_subscriptions, &block)
end
# Returns true if there are unprocessed presence updates waiting in the
# queue, false otherwise.
def new_subscriptions?
!queue(:new_subscriptions).empty?
end
# Returns an array of subscription notifications received since the last
# time subscription_requests was called. Passing a block will yield each update
# in turn, allowing you to break part-way through processing (especially
# useful when your subscription handling code is not thread-safe (e.g.,
# ActiveRecord).
#
# e.g.:
#
# jabber.subscription_requests do |friend, presence|
# puts "Received presence update from #{friend.to_s}: #{presence}"
# end
def subscription_requests(&block)
dequeue(:subscription_requests, &block)
end
# Returns true if auto-accept subscriptions (friend requests) is enabled
# (default), false otherwise.
def accept_subscriptions?
@accept_subscriptions = true if @accept_subscriptions.nil?
@accept_subscriptions
end
# Change whether or not subscriptions (friend requests) are automatically accepted.
def accept_subscriptions=(accept_status)
@accept_subscriptions = accept_status
end
# Direct access to the underlying Roster helper.
def roster
return @roster if @roster
self.roster = Roster::Helper.new(client)
end
# Direct access to the underlying Jabber client.
def client
connect!() unless connected?
@client
end
# Send a Jabber stanza over-the-wire.
def send!(msg)
attempts = 0
begin
attempts += 1
client.send(msg)
rescue Errno::EPIPE, IOError => e
sleep 1
disconnect
reconnect
retry unless attempts > 3
raise e
rescue Errno::ECONNRESET => e
sleep (attempts^2) * 60 + 60
disconnect
reconnect
retry unless attempts > 3
raise e
end
end
# Use this to force the client to reconnect after a force_disconnect.
def reconnect
@disconnected = false
connect!
end
# Use this to force the client to disconnect and not automatically
# reconnect.
def disconnect
disconnect!
end
# Queue messages for delivery once a user has accepted our authorization
# request. Works in conjunction with the deferred delivery thread.
#
# You can use this method if you want to manually add friends and still
# have the message queued for later delivery.
def deliver_deferred(jid, message, type)
msg = {:to => jid, :message => message, :type => type}
queue(:pending_messages) << [msg]
end
private
def client=(client)
self.roster = nil # ensure we clear the roster, since that's now associated with a different client.
@client = client
end
def roster=(new_roster)
@roster = new_roster
end
def connect!
raise ConnectionError, "Connections are disabled - use Jabber::Simple::force_connect() to reconnect." if @disconnected
# Pre-connect
@connect_mutex ||= Mutex.new
# don't try to connect if another thread is already connecting.
return if @connect_mutex.locked?
@connect_mutex.lock
disconnect!(false) if connected?
# Connect
jid = JID.new(@jid)
my_client = Client.new(@jid)
my_client.connect
my_client.auth(@password)
self.client = my_client
# Post-connect
register_default_callbacks
status(@presence, @status_message)
@connect_mutex.unlock
end
def disconnect!(auto_reconnect = true)
if client.respond_to?(:is_connected?) && client.is_connected?
begin
client.close
rescue Errno::EPIPE, IOError => e
# probably should log this.
nil
end
end
client = nil
@disconnected = auto_reconnect
end
def register_default_callbacks
client.add_message_callback do |message|
queue(:received_messages) << message unless message.body.nil?
end
roster.add_subscription_callback do |roster_item, presence|
if presence.type == :subscribed
queue(:new_subscriptions) << [roster_item, presence]
end
end
roster.add_subscription_request_callback do |roster_item, presence|
if accept_subscriptions?
roster.accept_subscription(presence.from)
else
queue(:subscription_requests) << [roster_item, presence]
end
end
@presence_updates = {}
@presence_mutex = Mutex.new
roster.add_presence_callback do |roster_item, old_presence, new_presence|
simple_jid = roster_item.jid.strip.to_s
presence = case new_presence.type
when nil: new_presence.show || :online
when :unavailable: :unavailable
else
nil
end
if presence && @presence_updates[simple_jid] != presence
queue(:presence_updates) << simple_jid
@presence_mutex.synchronize { @presence_updates[simple_jid] = [presence, new_presence.status] }
end
end
end
# This thread facilitates the delivery of messages to users who haven't yet
# accepted an invitation from us. When we attempt to deliver a message, if
# the user hasn't subscribed, we place the message in a queue for later
# delivery. Once a user has accepted our authorization request, we deliver
# any messages that have been queued up in the meantime.
def start_deferred_delivery_thread #:nodoc:
Thread.new {
loop {
messages = [queue(:pending_messages).pop].flatten
messages.each do |message|
if subscribed_to?(message[:to])
deliver(message[:to], message[:message], message[:type])
else
queue(:pending_messages) << message
end
end
}
}
end
def queue(queue)
@queues ||= Hash.new { |h,k| h[k] = Queue.new }
@queues[queue]
end
def dequeue(queue, non_blocking = true, max_items = 100, &block)
queue_items = []
max_items.times do
queue_item = queue(queue).pop(non_blocking) rescue nil
break if queue_item.nil?
queue_items << queue_item
yield queue_item if block_given?
end
queue_items
end
end
end
true