require File.join(File.dirname(__FILE__), *%w[.. .. blather])
module Blather
# # Blather Client
#
# Blather's Client class provides a set of helpers for working with common
# XMPP tasks such as setting up and starting the connection, settings
# status, registering and dispatching filters and handlers and roster
# management.
#
# Client can be used separately from the DSL if you'd like to implement your
# own DSL Here's the echo example using the client without the DSL:
#
# require 'blather/client/client'
# client = Client.setup 'echo@jabber.local', 'echo'
#
# client.register_handler(:ready) do
# puts "Connected ! send messages to #{client.jid.stripped}."
# end
#
# client.register_handler :subscription, :request? do |s|
# client.write s.approve!
# end
#
# client.register_handler :message, :chat?, :body => 'exit' do |m|
# client.write Blather::Stanza::Message.new(m.from, 'Exiting...')
# client.close
# end
#
# client.register_handler :message, :chat?, :body do |m|
# client.write Blather::Stanza::Message.new(m.from, "You sent: #{m.body}")
# end
#
class Client
attr_reader :jid,
:roster,
:caps
# Create a new client and set it up
#
# @param [Blather::JID, #to_s] jid the JID to authorize with
# @param [String] password the password to authorize with
# @param [String] host if this isn't set it'll be resolved off the JID's
# domain
# @param [Fixnum, String] port the port to connect to.
#
# @return [Blather::Client]
def self.setup(jid, password, host = nil, port = nil)
self.new.setup(jid, password, host, port)
end
def initialize # @private
@state = :initializing
@status = Stanza::Presence::Status.new
@handlers = {}
@tmp_handlers = {}
@filters = {:before => [], :after => []}
@roster = Roster.new self
@caps = Caps.new
setup_initial_handlers
end
# Get the current status. Taken from the `state` attribute of Status
def status
@status.state
end
# Set the status. Status can be set with either a single value or an array
# containing
#
# [state, message, to].
def status=(state)
state, msg, to = state
status = Stanza::Presence::Status.new state, msg
status.to = to
@status = status unless to
write status
end
# Start the connection.
#
# The stream type used is based on the JID. If a node exists it uses
# Blather::Stream::Client otherwise Blather::Stream::Component
def run
raise 'not setup!' unless setup?
klass = @setup[0].node ? Blather::Stream::Client : Blather::Stream::Component
klass.start self, *@setup
end
alias_method :connect, :run
# Register a filter to be run before or after the handler chain is run.
#
# @param [<:before, :after>] type the filter type
# @param [Symbol, nil] handler set the filter on a specific handler
# @param [guards] guards take a look at the guards documentation
# @yield [Blather::Stanza] stanza the incomming stanza
def register_filter(type, handler = nil, *guards, &filter)
unless [:before, :after].include?(type)
raise "Invalid filter: #{type}. Must be :before or :after"
end
@filters[type] << [guards, handler, filter]
end
# Register a temporary handler. Temporary handlers are based on the ID of
# the JID and live only until a stanza with said ID is received.
#
# @param [#to_s] id the ID of the stanza that should be handled
# @yield [Blather::Stanza] stanza the incomming stanza
def register_tmp_handler(id, &handler)
@tmp_handlers[id.to_s] = handler
end
# Clear handlers with given guards
#
# @param [Symbol, nil] type remove filters for a specific handler
# @param [guards] guards take a look at the guards documentation
def clear_handlers(type, *guards)
@handlers[type].delete_if { |g, _| g == guards }
end
# Register a handler
#
# @param [Symbol, nil] type set the filter on a specific handler
# @param [guards] guards take a look at the guards documentation
# @yield [Blather::Stanza] stanza the incomming stanza
def register_handler(type, *guards, &handler)
check_handler type, guards
@handlers[type] ||= []
@handlers[type] << [guards, handler]
end
# Write data to the stream
#
# @param [#to_xml, #to_s] stanza the content to send down the wire
def write(stanza)
self.stream.send(stanza)
end
# Helper that will create a temporary handler for the stanza being sent
# before writing it to the stream.
#
# client.write_with_handler(stanza) { |s| "handle stanza here" }
#
# is equivalent to:
#
# client.register_tmp_handler(stanza.id) { |s| "handle stanza here" }
# client.write stanza
#
# @param [Blather::Stanza] stanza the stanza to send down the wire
# @yield [Blather::Stanza] stanza the reply stanza
def write_with_handler(stanza, &handler)
register_tmp_handler stanza.id, &handler
write stanza
end
# Close the connection
def close
self.stream.close_connection_after_writing
end
def post_init(stream, jid = nil) # @private
@stream = stream
@jid = JID.new(jid) if jid
self.jid.node ? client_post_init : ready!
end
def unbind # @private
call_handler_for(:disconnected, nil) || (EM.reactor_running? && EM.stop)
end
def receive_data(stanza) # @private
catch(:halt) do
run_filters :before, stanza
handle_stanza stanza
run_filters :after, stanza
end
end
def setup? # @private
@setup.is_a? Array
end
def setup(jid, password, host = nil, port = nil) # @private
@jid = JID.new(jid)
@setup = [@jid, password]
@setup << host if host
@setup << port if port
self
end
protected
def stream # @private
@stream || raise('Stream not ready!')
end
def check_handler(type, guards) # @private
Blather.logger.warn "Handler for type \"#{type}\" will never be called as it's not a registered type" unless current_handlers.include?(type)
check_guards guards
end
def current_handlers # @private
[:ready, :disconnected] + Stanza.handler_list + BlatherError.handler_list
end
def setup_initial_handlers # @private
register_handler :error do |err|
raise err
end
# register_handler :iq, :type => [:get, :set] do |iq|
# write StanzaError.new(iq, 'service-unavailable', :cancel).to_node
# end
register_handler :status do |status|
roster[status.from].status = status if roster[status.from]
nil
end
register_handler :roster do |node|
roster.process node
end
end
def ready! # @private
@state = :ready
call_handler_for :ready, nil
end
def client_post_init # @private
write_with_handler Stanza::Iq::Roster.new do |node|
roster.process node
write @status
ready!
end
end
def run_filters(type, stanza) # @private
@filters[type].each do |guards, handler, filter|
next if handler && !stanza.handler_hierarchy.include?(handler)
catch(:pass) { call_handler filter, guards, stanza }
end
end
def handle_stanza(stanza) # @private
if handler = @tmp_handlers.delete(stanza.id)
handler.call stanza
else
stanza.handler_hierarchy.each do |type|
break if call_handler_for(type, stanza)
end
end
end
def call_handler_for(type, stanza) # @private
return unless handler = @handlers[type]
handler.find do |guards, handler|
catch(:pass) { call_handler handler, guards, stanza }
end
end
def call_handler(handler, guards, stanza) # @private
if guards.first.respond_to?(:to_str)
result = stanza.find(*guards)
handler.call(stanza, result) unless result.empty?
else
handler.call(stanza) unless guarded?(guards, stanza)
end
end
# If any of the guards returns FALSE this returns true
# the logic is reversed to allow short circuiting
# (why would anyone want to loop over more values than necessary?)
#
# @private
def guarded?(guards, stanza)
guards.find do |guard|
case guard
when Symbol
!stanza.__send__(guard)
when Array
# return FALSE if any item is TRUE
!guard.detect { |condition| !guarded?([condition], stanza) }
when Hash
# return FALSE unless any inequality is found
guard.find do |method, test|
value = stanza.__send__(method)
# last_match is the only method found unique to Regexp classes
if test.class.respond_to?(:last_match)
!(test =~ value.to_s)
elsif test.is_a?(Array)
!test.include? value
else
test != value
end
end
when Proc
!guard.call(stanza)
end
end
end
def check_guards(guards) # @private
guards.each do |guard|
case guard
when Array
guard.each { |g| check_guards([g]) }
when Symbol, Proc, Hash, String
nil
else
raise "Bad guard: #{guard.inspect}"
end
end
end
class Caps < Blather::Stanza::DiscoInfo
def self.new
super :result
end
def ver
generate_ver identities, features
end
def node=(node)
@bare_node = node
super "#{node}##{ver}"
end
def identities=(identities)
super identities
regenerate_full_node
end
def features=(features)
super features
regenerate_full_node
end
def c
Blather::Stanza::Presence::C.new @bare_node, ver
end
private
def regenerate_full_node
self.node = @bare_node
end
def generate_ver_str(identities, features, forms = [])
# 1. Initialize an empty string S.
s = ''
# 2. Sort the service discovery identities by category and
# then by type (if it exists) and then by xml:lang (if it
# exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/'
# [NAME]. Note that each slash is included even if the TYPE,
# LANG, or NAME is not included.
identities.sort! do |identity1, identity2|
cmp_result = nil
[:category, :type, :xml_lang, :name].each do |field|
value1 = identity1.send(field)
value2 = identity2.send(field)
if value1 != value2
cmp_result = value1 <=> value2
break
end
end
cmp_result
end
# 3. For each identity, append the 'category/type/lang/name' to
# S, followed by the '<' character.
s += identities.collect do |identity|
[:category, :type, :xml_lang, :name].collect do |field|
identity.send(field).to_s
end.join('/') + '<'
end.join
# 4. Sort the supported service discovery features.
features.sort! { |feature1, feature2| feature1.var <=> feature2.var }
# 5. For each feature, append the feature to S, followed by the
# '<' character.
s += features.collect { |feature| feature.var.to_s + '<' }.join
# 6. If the service discovery information response includes
# XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e., by
# the XML character data of the element).
forms.sort! do |form1, form2|
fform_type1 = form1.field 'FORM_TYPE'
fform_type2 = form2.field 'FORM_TYPE'
form_type1 = fform_type1 ? fform_type1.values.to_s : nil
form_type2 = fform_type2 ? fform_type2.values.to_s : nil
form_type1 <=> form_type2
end
# 7. For each extended service discovery information form:
forms.each do |form|
# 7.1. Append the XML character data of the FORM_TYPE field's
# element, followed by the '<' character.
fform_type = form.field 'FORM_TYPE'
form_type = fform_type ? fform_type.values.to_s : nil
s += "#{form_type}<"
# 7.2. Sort the fields by the value of the "var" attribute
fields = form.fields.sort { |field1, field2| field1.var <=> field2.var }
# 7.3. For each field:
fields.each do |field|
# 7.3.1. Append the value of the "var" attribute, followed by
# the '<' character.
s += "#{field.var}<"
# 7.3.2. Sort values by the XML character data of the element
# values = field.values.sort { |value1, value2| value1 <=> value2 }
# 7.3.3. For each element, append the XML character
# data, followed by the '<' character.
# s += values.collect { |value| "#{value}<" }.join
s += "#{field.value}<"
end
end
s
end
def generate_ver(identities, features, forms = [], hash = 'sha-1')
s = generate_ver_str identities, features, forms
# 9. Compute the verification string by hashing S using the
# algorithm specified in the 'hash' attribute (e.g., SHA-1 as
# defined in RFC 3174). The hashed data MUST be generated
# with binary output and encoded using Base64 as specified in
# Section 4 of RFC 4648 (note: the Base64 output MUST NOT
# include whitespace and MUST set padding bits to zero).
# See http://www.iana.org/assignments/hash-function-text-names
hash_klass = case hash
when 'md2' then nil
when 'md5' then Digest::MD5
when 'sha-1' then Digest::SHA1
when 'sha-224' then nil
when 'sha-256' then Digest::SHA256
when 'sha-384' then Digest::SHA384
when 'sha-512' then Digest::SHA512
end
hash_klass ? [hash_klass::digest(s)].pack('m').strip : nil
end
end # Caps
end # Client
end # Blather